├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .storybook ├── addons.js └── config.js ├── .travis.yml ├── README.md ├── docs ├── BasicPicker.md ├── GradientPicker.md ├── README.md ├── SchemePicker.md └── color-conversion.md ├── examples ├── BasicPicker.js ├── GradientPicker.js └── SchemePicker.js ├── media ├── basic_picker.gif ├── clipboard.gif ├── dark.png ├── darken.gif ├── demo.gif ├── desaturation.gif ├── format.gif ├── gradient-dark.png ├── gradient-generator.gif ├── gradient-light.png ├── image.gif ├── light.png ├── lighten.gif ├── reset.gif ├── saturation.gif ├── scheme.gif ├── spin.gif ├── swatches.gif └── tints.gif ├── package.json ├── rollup.config.js ├── src ├── components │ ├── ColorBlock.js │ ├── ColorFormatPicker.js │ ├── ColorInputField.js │ ├── CompactSwatches.js │ ├── Container.js │ ├── Image.js │ ├── Slider.js │ ├── Swatch.js │ ├── Swatches.js │ ├── Tools.js │ └── Triangle.js ├── icons │ └── index.js ├── index.js ├── pickers │ ├── BasicPicker.js │ ├── GradientPicker.js │ └── SchemePicker.js ├── styles │ └── tooltip.css └── utils │ ├── colors.js │ ├── constants.js │ ├── context.js │ └── theme.js ├── stories └── index.js ├── test-setup.js ├── tests ├── Basic-Picker.test.js ├── Gradient-Picker.test.js ├── Scheme-Picker.test.js ├── __snapshots__ │ ├── Basic-Picker.test.js.snap │ ├── Gradient-Picker.test.js.snap │ └── Scheme-Picker.test.js.snap └── styleMock.js ├── website ├── package.json ├── public │ └── index.html ├── src │ └── index.js └── yarn.lock └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [ 5 | "env", 6 | "react" 7 | ], 8 | "plugins": [ 9 | "transform-object-rest-spread", 10 | "transform-class-properties" 11 | ] 12 | }, 13 | "production": { 14 | "presets": [ 15 | ["env", { "modules": false, "loose": true }], 16 | "react" 17 | ], 18 | "plugins": [ 19 | "transform-object-rest-spread", 20 | "transform-class-properties", 21 | "transform-react-remove-prop-types" 22 | ] 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | examples 2 | media 3 | public 4 | node_modules 5 | website 6 | docs 7 | src/test.js 8 | stories 9 | tests -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'airbnb', 3 | rules: { 4 | 'import/no-extraneous-dependencies': 0, 5 | semi: 0, 6 | 'react/jsx-filename-extension': 0, 7 | 'comma-dangle': 0, 8 | 'implicit-arrow-linebreak': 0, 9 | 'arrow-parens': 0, 10 | 'react/destructuring-assignment': 0, 11 | 'prefer-destructuring': 0, 12 | 'no-undef': 0, 13 | 'no-unused-expressions': 0, 14 | 'no-restricted-globals': 0, 15 | 'object-curly-newline': 0, 16 | 'jsx-a11y/no-noninteractive-tabindex': 0, 17 | 'jsx-a11y/no-static-element-interactions': 0, 18 | 'jsx-a11y/aria-role': 0, 19 | 'no-confusing-arrow': 0, 20 | 'jsx-a11y/label-has-associated-control': 0, 21 | 'jsx-a11y/label-has-for': 0, 22 | 'react/no-did-update-set-state': 0, 23 | radix: 0, 24 | 'no-nested-ternary': 0, 25 | 'operator-linebreak': 0, 26 | 'no-unexpected-multiline': 0, 27 | 'function-paren-newline': 0 28 | }, 29 | parser: 'babel-eslint' 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | dist 3 | node_modules 4 | build 5 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register' 2 | import '@storybook/addon-links/register' 3 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react' 2 | 3 | function loadStories() { 4 | require('../stories') 5 | } 6 | 7 | configure(loadStories, module) 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | notifications: 5 | email: false 6 | script: 7 | - yarn lint 8 | - yarn test 9 | - yarn build 10 | cache: yarn -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-color-tools 2 | 3 | [![Build Status](https://travis-ci.org/nitin42/react-color-tools.svg?branch=master)](https://travis-ci.org/nitin42/react-color-tools) 4 | 5 | > A set of tools as React components for working with colors 6 | 7 |

8 | 9 |

10 | 11 | ## Table of contents 12 | 13 | - [Introduction](#introduction) 14 | 15 | - [Motivation](#motivation) 16 | 17 | - [Features](#features) 18 | 19 | - [Use cases](#use-cases) 20 | 21 | - [Theory](#theory) 22 | 23 | - [Install](#install) 24 | 25 | - [Usage](#usage) 26 | 27 | - [Examples](#examples) 28 | 29 | - [Documentation](#documentation) 30 | 31 | - [Contributing](#contributing) 32 | 33 | - [License](#license) 34 | 35 | ## Introduction 36 | 37 | `react-color-tools` provides a set of tools as React components for working with colors. These tools can be used to manipulate a color for example controlling the intensity or purity of color, extracting swatches from an image, creating a gradienty defining color stop positions, choosing from variety of shades and tints or choosing a color scheme. 38 | 39 | ## Motivation 40 | 41 | `react-color-tools` is inspired from [`react-color`](https://github.com/casesandberg/react-color). I was using `react-color` for my projects and felt the need for more features like [image color extraction](https://react-color-extractor.surge.sh), generating shades and tints, creating gradients, and advance color tools for controlling the intensity and value of the color. So I decided to build `react-color-tools` with these features while keeping the API surface minimal and easy to use. 42 | 43 | ## Features 44 | 45 | - Image color extraction 46 | 47 | - Generate shades and tints 48 | 49 | - Built-in color manipulation tools 50 | 51 | - Create gradient by controlling the color stop positions 52 | 53 | - API for color conversions 54 | 55 | - Color scheme picker 56 | 57 | ## Use cases 58 | 59 | - Managing color schemes 60 | 61 | - Design systems & creating design tools 62 | 63 | ## Theory 64 | 65 | A little bit about different color terms and color harmonies that you will be using while working with `react-color-tools`. 66 | 67 |

68 | 69 |

70 | 71 | > Image taken from Canva 72 | 73 | ### Color terms 74 | 75 | - **Hue** - A hue is name of particular color, or it is also one of the 12 colors on the color wheel. 76 | 77 | - **Shade** - A shade is a hue darkened with black. 78 | 79 | - **Tint** - A tint is a hue lightened with gray. 80 | 81 | - **Saturation** - Describes the intensity or purity of color. 82 | 83 | - **Desaturation** - Desaturation makes a color look more muted (hue approaches closer to gray). 84 | 85 | ### Color schemes 86 | 87 | - **Monochromatic** - This color scheme contains tints, shades and tones of a color. 88 | 89 | - **Analogous** - This color scheme contains the hues that are located side by side on the color wheel. 90 | 91 | - **Split Complementary** - This scheme represent any color on the color wheel plus two colors that are it's complement. 92 | 93 | - **Triadic** - This color scheme has three colors that are evenly spaced on the color wheel. 94 | 95 | - **Tetradic** - Two pairs of colors which are opposite on the color wheel 96 | 97 | ## Install 98 | 99 | ``` 100 | npm install react-color-tools 101 | ``` 102 | 103 | or if you use yarn, 104 | 105 | ``` 106 | yarn add react-color-tools 107 | ``` 108 | 109 | **This package also depends on React so make sure you've it installed.** 110 | 111 | ## Usage 112 | 113 | ```js 114 | import React from 'react' 115 | import { render } from 'react-dom' 116 | import { BasicPicker } from 'react-color-tools' 117 | 118 | class App extends React.Component { 119 | state = { 120 | color: 'hotpink' 121 | } 122 | 123 | render() { 124 | return ( 125 |
126 | this.setState({ color })} 129 | /> 130 |

React Color Tools

131 |
132 | ) 133 | } 134 | } 135 | ``` 136 | 137 | This will render - 138 | 139 |

140 | 141 |

142 | 143 | ## Examples 144 | 145 | #### Basic Picker 146 | 147 | [![Edit jj7jpl5xvv](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/jj7jpl5xvv) 148 | 149 | #### Gradient Picker 150 | 151 | [![Edit wqln38j8wk](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/wqln38j8wk) 152 | 153 | #### Scheme Picker 154 | 155 | [![Edit 935ppx461o](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/935ppx461o) 156 | 157 | or checkout the [`examples`](./examples) folder 158 | 159 | ## Documentation 160 | 161 | [Check out the detailed documentation](./docs) 162 | 163 | ## TODO 164 | 165 | - [ ] Use a monorepo format to store different pickers and color tools 166 | 167 | - [ ] Tweak rollup config to reduce bundle size 168 | 169 | - [ ] Remove [tooltip styles](./src/styles) and use `react-emotion` to create tooltip component 170 | 171 | - [ ] Add bezier easing to gradient picker component 172 | 173 | ## Contributing 174 | 175 | If you'd like to contribute to this project, then follow the below instructions to setup the project locally on your machine. 176 | 177 | ``` 178 | git clone https://github.com//react-color-tools 179 | 180 | cd react-color-tools 181 | 182 | yarn 183 | ``` 184 | 185 | ### Linting 186 | 187 | Run eslint using `yarn lint` 188 | 189 | ### Building the source code 190 | 191 | Run `yarn build` to build the source code. To use the watch mode, run the cmd `yarn build:watch` 192 | 193 | ### Formatting with Prettier 194 | 195 | Run `yarn formatall` to format the source code. 196 | 197 | ### Storybook 198 | 199 | Run `yarn storybook` to start the storybook development environment. 200 | 201 | ### Test 202 | 203 | Run `yarn test` to test the pickers. 204 | 205 | ## License 206 | 207 | MIT 208 | -------------------------------------------------------------------------------- /docs/BasicPicker.md: -------------------------------------------------------------------------------- 1 | # Basic Color Picker 2 | 3 |

4 | 5 |

6 | 7 | ## `` 8 | 9 | Basic color picker includes tools such as - 10 | 11 | - Image color extraction 12 | 13 | - Generating shades and tints 14 | - Built-in color manipulation tools 15 | 16 | ## Usage 17 | 18 | ```jsx 19 | import React from 'react' 20 | import { BasicPicker } from 'react-color-tools' 21 | 22 | class App extends React.Component { 23 | state = { 24 | color: 'hotpink' 25 | } 26 | 27 | render() { 28 | return ( 29 |
30 | this.setState({ color })} 33 | /> 34 |

React Color Tools

35 |
36 | ) 37 | } 38 | } 39 | ``` 40 | 41 | ## Component API 42 | 43 | `color: string` 44 | 45 | color prop represents what color is currently active in the color picker. Use this prop to initialize the color picker with a particular color, or to keep it in sync with the state of a parent component. The default value is `#088da5` 46 | 47 | ```jsx 48 | 49 | ``` 50 | 51 | `onChange: (color: string): void => {}` 52 | 53 | This is invoked everytime when a color is updated in the color picker for example - clicking a swatch, extracting colors from image, generating swatches, shades or tints. Use this callback to update the state of parent component with the currently active color. 54 | 55 | ```jsx 56 |
57 | this.setState({ color })} 60 | /> 61 |

Color Tools

62 |
63 | ``` 64 | 65 | `swatches: Array` 66 | 67 | Initialize your own swatches in the color picker by passing an array of colors in either hex format or specifying the name of color. 68 | 69 | ```jsx 70 | 71 | ``` 72 | 73 | `onSwatchHover: (color: string): void => {}` 74 | 75 | Similar to `onChange` callback. The only difference is, this is invoked on hovering over a color in the color picker. 76 | 77 | `theme: string` 78 | 79 | theme prop accepts two values - `light` and `dark`. Use this prop to set the theme of the color picker. The default value is `light` 80 | 81 | **Light theme** 82 | 83 |

84 | 85 |

86 | 87 | **Dark theme** 88 | 89 |

90 | 91 |

92 | 93 | `maxColors: number` 94 | 95 | This prop accepts a number for amount of colors in palette from which swatches will be generated from an image. The default value is `64` 96 | 97 | `showTools: boolean` 98 | 99 | When set to true, will add advance color manipulation tools to the color picker. These tools include - 100 | 101 | - **Color spin** - spin (change) the color by a degree amount 102 | 103 |

104 | 105 |

106 | 107 | - **Color desaturation** - making the color more muted or closer to gray 108 | 109 |

110 | 111 |

112 | 113 | - **Color saturation** - changing the intensity or purity of color 114 | 115 |

116 | 117 |

118 | 119 | - **Color darkening** - darken a color by an amount 120 | 121 |

122 | 123 |

124 | 125 | - **Color brightening** - brighten a color by an amount 126 | 127 |

128 | 129 |

130 | 131 | `triangle: boolean` 132 | 133 | When set to `false`, will remove the triangle from the top of color picker 134 | 135 | ### Image color extraction 136 | 137 | You can also extract swatches from an image. 138 | 139 |

140 | 141 |

142 | 143 | ### Generating shades and tints 144 | 145 | Generate shades and tints for a color 146 | 147 |

148 | 149 |

150 | 151 | ## Copy a color from picker 152 | 153 | You can also directly copy the color in either format from the picker. 154 | 155 |

156 | 157 |

158 | 159 | ## Display previous swatches 160 | 161 | You can also go back to the previous swatches after selecting the shades and tints of a color. 162 | 163 |

164 | 165 |

166 | 167 | ## Change color format 168 | 169 | By default, the color format in the color picker is hex. To change the format, use the dropdown to select a color format option. 170 | 171 |

172 | 173 |

174 | 175 | ## Generate different swatches 176 | 177 | You can generate a different set of swatches to choose from inside the color picker 178 | 179 |

180 | 181 |

182 | -------------------------------------------------------------------------------- /docs/GradientPicker.md: -------------------------------------------------------------------------------- 1 | # Gradient Picker 2 | 3 |

4 | 5 |

6 | 7 | ## `` 8 | 9 | `` component is used to create a gradient by mixing two colors and changing their stop positions. 10 | 11 | ### Color stops 12 | 13 | A gradient is created by changing the color stops. Color stops are stopping points in a gradient that show a specific color at the exact location you set. 14 | 15 | ## Component API 16 | 17 | `colorOne: string` 18 | 19 | input value for color one. It can be a color name, hex code, a rgb or rgba string, or a hsl string. The default value is `#81FFEF`. 20 | 21 | `colorTwo: string` 22 | 23 | input value for color two. Accepts values similar to `colorOne`. 24 | 25 | `getGradient: (gradient: string): void => {}` 26 | 27 | This is invoked on updating color input field and color stop positions. The callback function receives the css gradient string as its value and can be used to keep the state of parent component in sync. 28 | 29 | ### Usage 30 | 31 | ```jsx 32 | import React from 'react' 33 | import { GradientPicker } from 'gradientPicker' 34 | 35 | class App extends React.Component { 36 | state = { gradient: '' } 37 | 38 | render() { 39 | const { gradient: grad } = this.state 40 | 41 | return ( 42 |
43 | this.setState({ gradient })} /> 44 |

51 | React Gradient Picker 52 |

53 |
54 | ) 55 | } 56 | } 57 | ``` 58 | 59 | `theme: string` 60 | 61 | theme prop accepts two values - `light` and `dark`. Use this prop to set the theme of the color picker. The default value is `light` 62 | 63 | **Light theme** 64 | 65 |

66 | 67 |

68 | 69 | **Dark theme** 70 | 71 |

72 | 73 |

74 | 75 | `reverse: boolean` 76 | 77 | When set to `true`, reverses the gradient. The default value is `false`. 78 | 79 | `mode: string` 80 | 81 | Use this prop to specify the gradient mode. It accepts only two values - `linear` or `radial` 82 | 83 | `direction: string` 84 | 85 | Use this prop to specify the direction of gradient. For example - `to right` or `to left` when the gradient mode is `linear`. 86 | 87 | ```jsx 88 | /* do some stuff with gradient */} mode='linear' direction='to left' /> 89 | ``` 90 | 91 | ## Picker tools 92 | 93 | ### Copy gradient string 94 | 95 | Similar to `BasicColorPicker`, you can copy the css gradient string from the picker itself. 96 | 97 | ### Generating different gradients 98 | 99 | You can generate different gradients and adjust their stop positions accordingly. 100 | 101 |

102 | 103 |

104 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | This is the documentation for `react-color-tools` 4 | 5 | ## Table of contents 6 | 7 | - [Introduction](https://github.com/nitin42/react-color-tools/tree/new-scheme-picker#introduction) 8 | 9 | - [Features](https://github.com/nitin42/react-color-tools/tree/new-scheme-picker#features) 10 | 11 | - [Usage](https://github.com/nitin42/react-color-tools/tree/new-scheme-picker#usage) 12 | 13 | - [Install](https://github.com/nitin42/react-color-tools/tree/new-scheme-picker#install) 14 | 15 | - [Basic color picker](./BasicPicker.md) 16 | 17 | - [Component API](https://github.com/nitin42/react-color-tools/blob/new-scheme-picker/docs/BasicPicker.md#component-api) 18 | 19 | - [Usage](https://github.com/nitin42/react-color-tools/blob/new-scheme-picker/docs/BasicPicker.md#usage) 20 | 21 | - [Image color extraction](https://github.com/nitin42/react-color-tools/blob/new-scheme-picker/docs/BasicPicker.md#image-color-extraction) 22 | 23 | - [Generating shades and tints](https://github.com/nitin42/react-color-tools/blob/new-scheme-picker/docs/BasicPicker.md#generating-shades-and-tints) 24 | 25 | - [Copying color from picker](https://github.com/nitin42/react-color-tools/blob/new-scheme-picker/docs/BasicPicker.md#copy-a-color-from-picker) 26 | 27 | - [Displaying previous swatches](https://github.com/nitin42/react-color-tools/blob/new-scheme-picker/docs/BasicPicker.md#display-previous-swatches) 28 | 29 | - [Generating different swatches](https://github.com/nitin42/react-color-tools/blob/new-scheme-picker/docs/BasicPicker.md#generate-different-swatches) 30 | 31 | - [Changing color format](https://github.com/nitin42/react-color-tools/blob/new-scheme-picker/docs/BasicPicker.md#change-color-format) 32 | 33 | - [Gradient picker](https://github.com/nitin42/react-color-tools/blob/new-scheme-picker/docs/GradientPicker.md#gradient-picker) 34 | 35 | - [Usage](https://github.com/nitin42/react-color-tools/blob/new-scheme-picker/docs/GradientPicker.md#usage) 36 | 37 | - [Component API](https://github.com/nitin42/react-color-tools/blob/new-scheme-picker/docs/GradientPicker.md#component-api) 38 | 39 | - [Gradient picker tools](https://github.com/nitin42/react-color-tools/blob/new-scheme-picker/docs/GradientPicker.md#picker-tools) 40 | 41 | - [Color scheme picker](https://github.com/nitin42/react-color-tools/blob/new-scheme-picker/docs/SchemePicker.md#color-scheme-picker) 42 | 43 | - [Component API](https://github.com/nitin42/react-color-tools/blob/new-scheme-picker/docs/SchemePicker.md#component-api) 44 | 45 | - [Usage](https://github.com/nitin42/react-color-tools/blob/new-scheme-picker/docs/SchemePicker.md#usage) 46 | 47 | - [Color schemes and color theory](https://github.com/nitin42/react-color-tools/blob/new-scheme-picker/README.md#theory) 48 | 49 | - [Color conversion APIs](./color-conversion.md) 50 | 51 | - [Examples](../examples) 52 | 53 | - [Contributing](https://github.com/nitin42/react-color-tools/blob/new-scheme-picker/README.md#contributing) 54 | 55 | - [License](https://github.com/nitin42/react-color-tools/blob/new-scheme-picker/README.md#license) 56 | -------------------------------------------------------------------------------- /docs/SchemePicker.md: -------------------------------------------------------------------------------- 1 | # Color Scheme Picker 2 | 3 |

4 | 5 |

6 | 7 | ## `` 8 | 9 | Scheme picker component displays an array of swatches for an input color to help you choose the color based on a color scheme. 10 | 11 | [Learn about color schemes](../README.md#theory) 12 | 13 | ## Component API 14 | 15 | `color: string` 16 | 17 | color prop represents what color is currently active in the color picker. Use this prop to initialize the color picker with a particular color, or to keep it in sync with the state of a parent component. The default value is `#088da5` 18 | 19 | `onChange: (color: string): void => {}` 20 | 21 | This is invoked everytime when a color is updated in the color picker for example - clicking a swatch or changing the color input. 22 | 23 | `theme: string` 24 | 25 | theme prop accepts two values - `light` and `dark`. Use this prop to set the theme of the color picker. The default value is `light` 26 | 27 | `scheme: string` 28 | 29 | scheme prop accepts a type of color scheme. For example - `monochromatic`. It supports the following values - 30 | 31 | - `monochromatic` 32 | 33 | - `analogous` 34 | 35 | - `splitcomplement` 36 | 37 | - `triad` 38 | 39 | - `tetrad` 40 | 41 | ## Usage 42 | 43 | ```jsx 44 | import React from 'react' 45 | import { SchemePicker } from 'react-color-tools' 46 | 47 | class App extends React.Component { 48 | state = { color: 'hotpink' } 49 | 50 | render() { 51 | return ( 52 |
60 |

React Color Tools

61 | this.setState({ color })} 66 | /> 67 |
68 | ) 69 | } 70 | } 71 | ``` 72 | 73 | > For analogous and monochromatic schemes, you can scroll through the list of swatches to see all the available colors. 74 | -------------------------------------------------------------------------------- /docs/color-conversion.md: -------------------------------------------------------------------------------- 1 | ## Color conversion APIs 2 | 3 | By default, the color format in color picker is hex. To convert a color from one format to another, use the `utils` object which is available as named export. 4 | 5 | ```js 6 | import { utils } from 'react-color-tools' 7 | ``` 8 | 9 | For example - The default format for color when `onChange` is invoked is hex. 10 | 11 | ```jsx 12 | state = { color: 'red' } 13 | 14 | { 15 | this.setState({ color }); 16 | console.log(color); // #F00 17 | }}/> 18 | ``` 19 | 20 | --- 21 | 22 | **`toRGB(color)`** 23 | 24 | To convert a color to rgb format, use the method `toRGB(color)` 25 | 26 | ``` 27 | utils.toRGB(color) 28 | ``` 29 | 30 | Example - 31 | 32 | ```jsx 33 | import React from 'react' 34 | import { BasicPicker, utils } from 'react-color-tools' 35 | 36 | class App extends React.Component { 37 | state = { 38 | color: 'hotpink' 39 | } 40 | 41 | render() { 42 | return ( 43 |
44 | this.setState({ color: utils.toRGB(color) })} 47 | /> 48 |

React Color Tools

49 |
50 | ) 51 | } 52 | } 53 | ``` 54 | 55 | Similarly for other formats, 56 | 57 | **`toHSL`** 58 | 59 | ``` 60 | utils.toHSL(color) 61 | ``` 62 | 63 | **`toHSV`** 64 | 65 | ``` 66 | utils.toHSV(color) 67 | ``` 68 | 69 | **`toRGBPercent`** 70 | 71 | ``` 72 | utils.toRGBPercent(color) 73 | ``` 74 | 75 | ### 76 | -------------------------------------------------------------------------------- /examples/BasicPicker.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BasicPicker } from 'react-color-tools' 3 | 4 | class App extends React.Component { 5 | state = { 6 | color: 'hotpink' 7 | } 8 | 9 | render() { 10 | const { color } = this.state 11 | 12 | return ( 13 |
14 | this.setState({ color })} 17 | theme="dark" 18 | showTools={true} 19 | /> 20 |

React Color Tools

21 |
22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/GradientPicker.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { GradientPicker } from 'react-color-tools' 3 | 4 | class App extends React.Component { 5 | state = { gradient: '' } 6 | 7 | render() { 8 | const { gradient: grad } = this.state 9 | 10 | return ( 11 |
12 | this.setState({ gradient })} /> 13 |

20 | React Gradient Picker 21 |

22 |
23 | ) 24 | } 25 | } 26 | 27 | // With gradient mode and direction 28 | class WithGradientMode extends React.Component { 29 | state = { gradient: '' } 30 | 31 | render() { 32 | const { gradient: grad } = this.state 33 | 34 | return ( 35 |
36 | this.setState({ gradient })} 40 | /> 41 |

48 | React Gradient Picker 49 |

50 |
51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/SchemePicker.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SchemePicker } from 'react-color-tools' 3 | 4 | class App extends React.Component { 5 | state = { color: 'hotpink' } 6 | 7 | render() { 8 | return ( 9 |
17 |

React Color Tools

18 | this.setState({ color })} 23 | /> 24 |
25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /media/basic_picker.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-color-tools/e909d21fc048b883504d8120cc8207b40d238cc5/media/basic_picker.gif -------------------------------------------------------------------------------- /media/clipboard.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-color-tools/e909d21fc048b883504d8120cc8207b40d238cc5/media/clipboard.gif -------------------------------------------------------------------------------- /media/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-color-tools/e909d21fc048b883504d8120cc8207b40d238cc5/media/dark.png -------------------------------------------------------------------------------- /media/darken.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-color-tools/e909d21fc048b883504d8120cc8207b40d238cc5/media/darken.gif -------------------------------------------------------------------------------- /media/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-color-tools/e909d21fc048b883504d8120cc8207b40d238cc5/media/demo.gif -------------------------------------------------------------------------------- /media/desaturation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-color-tools/e909d21fc048b883504d8120cc8207b40d238cc5/media/desaturation.gif -------------------------------------------------------------------------------- /media/format.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-color-tools/e909d21fc048b883504d8120cc8207b40d238cc5/media/format.gif -------------------------------------------------------------------------------- /media/gradient-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-color-tools/e909d21fc048b883504d8120cc8207b40d238cc5/media/gradient-dark.png -------------------------------------------------------------------------------- /media/gradient-generator.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-color-tools/e909d21fc048b883504d8120cc8207b40d238cc5/media/gradient-generator.gif -------------------------------------------------------------------------------- /media/gradient-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-color-tools/e909d21fc048b883504d8120cc8207b40d238cc5/media/gradient-light.png -------------------------------------------------------------------------------- /media/image.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-color-tools/e909d21fc048b883504d8120cc8207b40d238cc5/media/image.gif -------------------------------------------------------------------------------- /media/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-color-tools/e909d21fc048b883504d8120cc8207b40d238cc5/media/light.png -------------------------------------------------------------------------------- /media/lighten.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-color-tools/e909d21fc048b883504d8120cc8207b40d238cc5/media/lighten.gif -------------------------------------------------------------------------------- /media/reset.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-color-tools/e909d21fc048b883504d8120cc8207b40d238cc5/media/reset.gif -------------------------------------------------------------------------------- /media/saturation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-color-tools/e909d21fc048b883504d8120cc8207b40d238cc5/media/saturation.gif -------------------------------------------------------------------------------- /media/scheme.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-color-tools/e909d21fc048b883504d8120cc8207b40d238cc5/media/scheme.gif -------------------------------------------------------------------------------- /media/spin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-color-tools/e909d21fc048b883504d8120cc8207b40d238cc5/media/spin.gif -------------------------------------------------------------------------------- /media/swatches.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-color-tools/e909d21fc048b883504d8120cc8207b40d238cc5/media/swatches.gif -------------------------------------------------------------------------------- /media/tints.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-color-tools/e909d21fc048b883504d8120cc8207b40d238cc5/media/tints.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-color-tools", 3 | "version": "1.0.0", 4 | "description": "A set of tools as React components for working with colors", 5 | "main": "build/react-color-tools.js", 6 | "module": "build/react-color-tools.es.js", 7 | "unpkg": "build/react-color-tools.min.js", 8 | "author": "Nitin Tulswani", 9 | "license": "MIT", 10 | "files": [ 11 | "build" 12 | ], 13 | "dependencies": { 14 | "@ctrl/tinycolor": "^2.2.0", 15 | "clipboard-polyfill": "^2.5.4", 16 | "emotion": "^9.2.8", 17 | "prop-types": "^15.6.2", 18 | "randomcolor": "^0.5.3", 19 | "react-color-extractor": "^1.1.2", 20 | "react-emotion": "^9.2.10", 21 | "tinygradient": "^0.4.1", 22 | "values.js": "^1.0.3" 23 | }, 24 | "devDependencies": { 25 | "@storybook/addon-actions": "^3.4.11", 26 | "@storybook/addon-links": "^3.4.11", 27 | "@storybook/react": "^3.4.11", 28 | "babel-core": "^6.26.3", 29 | "babel-eslint": "^9.0.0", 30 | "babel-jest": "^23.6.0", 31 | "babel-plugin-external-helpers": "^6.22.0", 32 | "babel-plugin-transform-class-properties": "^6.24.1", 33 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 34 | "babel-plugin-transform-react-remove-prop-types": "^0.4.18", 35 | "babel-preset-env": "^1.7.0", 36 | "babel-preset-react": "^6.24.1", 37 | "enzyme": "^3.6.0", 38 | "enzyme-adapter-react-16": "^1.5.0", 39 | "eslint": "^4.19.1", 40 | "eslint-config-airbnb": "^17.1.0", 41 | "eslint-plugin-import": "^2.14.0", 42 | "eslint-plugin-jsx-a11y": "^6.1.1", 43 | "eslint-plugin-react": "^7.11.0", 44 | "husky": "^1.0.0-rc.13", 45 | "jest": "^23.6.0", 46 | "lint-staged": "^7.2.2", 47 | "node-sass": "^4.9.3", 48 | "prettier": "^1.14.2", 49 | "react": "^16.5.2", 50 | "react-dom": "^16.4.2", 51 | "react-test-renderer": "^16.5.2", 52 | "rollup": "^0.65.0", 53 | "rollup-plugin-babel": "^3.0.7", 54 | "rollup-plugin-filesize": "^4.0.1", 55 | "rollup-plugin-postcss": "^1.6.2", 56 | "rollup-plugin-replace": "^2.0.0", 57 | "rollup-plugin-uglify": "^4.0.0" 58 | }, 59 | "peerDependencies": { 60 | "react": "^16.3.2" 61 | }, 62 | "husky": { 63 | "hooks": { 64 | "pre-commit": "lint-staged" 65 | } 66 | }, 67 | "lint-staged": { 68 | "*.{js,json,css,md}": [ 69 | "prettier --write --no-semi --single-quote", 70 | "git add" 71 | ] 72 | }, 73 | "scripts": { 74 | "build:watch": "NODE_ENV=production rollup -c -w", 75 | "formatall": "find src -name '*.js' | xargs prettier --write --no-semi --single-quote", 76 | "lint": "eslint ./src", 77 | "build": "rm -rf ./build && NODE_ENV=production rollup -c", 78 | "validate": "yarn formatall && yarn lint && yarn test", 79 | "test:watch": "NODE_ENV=test ./node_modules/.bin/jest --watch", 80 | "test": "NODE_ENV=test ./node_modules/.bin/jest", 81 | "storybook": "NODE_ENV=test start-storybook -p 6006" 82 | }, 83 | "jest": { 84 | "testPathIgnorePatterns": [ 85 | "./test.js" 86 | ], 87 | "moduleNameMapper": { 88 | "\\.(css|less)$": "/tests/styleMock.js" 89 | }, 90 | "setupTestFrameworkScriptFile": "/test-setup.js" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel' 2 | import { uglify } from 'rollup-plugin-uglify' 3 | import replace from 'rollup-plugin-replace' 4 | import filesize from 'rollup-plugin-filesize' 5 | import postcss from 'rollup-plugin-postcss' 6 | 7 | import pkg from './package.json' 8 | 9 | const makeExternalPredicate = externalArr => { 10 | if (externalArr.length === 0) { 11 | return () => false 12 | } 13 | const pattern = new RegExp(`^(${externalArr.join('|')})($|/)`) 14 | return id => pattern.test(id) 15 | } 16 | 17 | const ensureArray = maybeArr => 18 | Array.isArray(maybeArr) ? maybeArr : [maybeArr] 19 | 20 | const createConfig = ({ output, min = false, env } = {}) => ({ 21 | input: 'src/index.js', 22 | output: ensureArray(output).map(format => 23 | Object.assign({}, format, { 24 | name: 'ReactColorTools', 25 | exports: 'named', 26 | globals: { 27 | react: 'React', 28 | '@ctrl/tinycolor': 'TinyColor', 29 | 'clipboard-polyfill': 'Clipboard', 30 | 'prop-types': 'PropTypes', 31 | randomcolor: 'generateColors', 32 | 'react-color-extractor': 'ColorExtractor', 33 | tinygradient: 'gradient', 34 | 'values.js': 'Values', 35 | emotion: 'emotion', 36 | 'react-emotion': 'styled' 37 | } 38 | }) 39 | ), 40 | external: makeExternalPredicate([ 41 | ...Object.keys(pkg.dependencies || {}), 42 | ...Object.keys(pkg.peerDependencies || {}) 43 | ]), 44 | plugins: [ 45 | postcss(), 46 | filesize(), 47 | babel({ plugins: ['external-helpers'] }), 48 | env && replace({ 'process.env.NODE_ENV': JSON.stringify(env) }), 49 | min && uglify() 50 | ].filter(Boolean) 51 | }) 52 | 53 | export default [ 54 | createConfig({ 55 | output: [ 56 | { file: pkg.main, format: 'cjs' }, 57 | { file: pkg.module, format: 'es' } 58 | ] 59 | }), 60 | createConfig({ 61 | output: { file: pkg.unpkg, format: 'umd' }, 62 | env: 'production', 63 | min: true 64 | }) 65 | ] 66 | -------------------------------------------------------------------------------- /src/components/ColorBlock.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { css } from 'emotion' 3 | import PropTypes from 'prop-types' 4 | 5 | import { getContrastingColor } from '../utils/colors' 6 | 7 | // Displays a color block with the active color hex code 8 | const ColorBlock = ({ currentFormat, colorState, color }) => ( 9 |
22 |
31 | {color} 32 |
33 |
34 | ) 35 | 36 | ColorBlock.defaultProps = { 37 | currentFormat: 'HEX', 38 | colorState: {} 39 | } 40 | 41 | ColorBlock.propTypes = { 42 | color: PropTypes.string.isRequired, 43 | /* eslint-disable react/forbid-prop-types */ 44 | colorState: PropTypes.object, 45 | currentFormat: PropTypes.string 46 | } 47 | 48 | export default ColorBlock 49 | -------------------------------------------------------------------------------- /src/components/ColorFormatPicker.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { css } from 'emotion' 3 | import PropTypes from 'prop-types' 4 | 5 | const renderFormats = formats => 6 | formats.map(format => ( 7 | 10 | )) 11 | 12 | const ColorFormatPicker = ({ changeFormat, formats }) => ( 13 |
20 | 38 |
39 | ) 40 | 41 | ColorFormatPicker.propTypes = { 42 | formats: PropTypes.arrayOf(PropTypes.string).isRequired, 43 | changeFormat: PropTypes.func.isRequired 44 | } 45 | 46 | export default ColorFormatPicker 47 | -------------------------------------------------------------------------------- /src/components/ColorInputField.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { css } from 'emotion' 3 | import PropTypes from 'prop-types' 4 | 5 | const INPUT_COLOR_SCHEME = 'rgb(102, 102, 102)' 6 | 7 | // Copied and edited from react-color/EditableInput 8 | class ColorInputField extends React.Component { 9 | state = { 10 | value: String(this.props.value).toUpperCase(), 11 | blurValue: String(this.props.value).toUpperCase() 12 | } 13 | 14 | componentWillReceiveProps(nextProps) { 15 | const input = this.input 16 | 17 | if (nextProps.value !== this.state.value) { 18 | if (input === document.activeElement) { 19 | this.setState({ blurValue: String(nextProps.value).toUpperCase() }) 20 | } else { 21 | this.setState(state => ({ 22 | value: String(nextProps.value).toUpperCase(), 23 | blurValue: !state.blurValue && String(nextProps.value).toUpperCase() 24 | })) 25 | } 26 | } 27 | } 28 | 29 | handleBlur = () => { 30 | if (this.state.blurValue) { 31 | this.setState(state => ({ value: state.blurValue, blurValue: null })) 32 | } 33 | } 34 | 35 | handleChange = e => { 36 | this.props.onChange && this.props.onChange(e.target.value, e) 37 | 38 | this.setState({ value: e.target.value }) 39 | } 40 | 41 | handleKeyDown = e => { 42 | const stringValue = String(e.target.value) 43 | const isPercentage = stringValue.indexOf('%') > -1 44 | const number = Number(stringValue.replace(/%/g, '')) 45 | 46 | if (!isNaN(number)) { 47 | const amount = 1 48 | 49 | if (e.keyCode === 38) { 50 | this.props.onChange && this.props.onChange(number + amount, e) 51 | 52 | if (isPercentage) { 53 | this.setState({ value: `${number + amount}%` }) 54 | } else { 55 | this.setState({ value: number + amount }) 56 | } 57 | } 58 | 59 | // Down 60 | if (e.keyCode === 40) { 61 | this.props.onChange && this.props.onChange(number - amount, e) 62 | 63 | if (isPercentage) { 64 | this.setState({ value: `${number - amount}%` }) 65 | } else { 66 | this.setState({ value: number - amount }) 67 | } 68 | } 69 | } 70 | } 71 | 72 | render() { 73 | return ( 74 | (this.input = input)} 90 | value={this.state.value} 91 | onKeyDown={this.handleKeyDown} 92 | onChange={this.handleChange} 93 | onBlur={this.handleBlur} 94 | placeholder={this.props.placeholder} 95 | spellCheck="false" 96 | /> 97 | ) 98 | } 99 | } 100 | 101 | ColorInputField.defaultProps = { 102 | value: '', 103 | onChange: () => {}, 104 | placeholder: '' 105 | } 106 | 107 | ColorInputField.propTypes = { 108 | value: PropTypes.string, 109 | onChange: PropTypes.func, 110 | placeholder: PropTypes.string 111 | } 112 | 113 | export default ColorInputField 114 | -------------------------------------------------------------------------------- /src/components/CompactSwatches.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { css } from 'emotion' 3 | import PropTypes from 'prop-types' 4 | 5 | const ENTER_KEY = 13 6 | 7 | const Swatch = ({ color, updateSwatch }) => ( 8 |
updateSwatch(color)} 15 | onKeyDown={e => (e.keyCode === ENTER_KEY ? updateSwatch(color) : null)} 16 | /> 17 | ) 18 | 19 | Swatch.propTypes = { 20 | color: PropTypes.string.isRequired, 21 | updateSwatch: PropTypes.func.isRequired 22 | } 23 | 24 | const CompactSwatches = ({ schemes, updateSwatch }) => ( 25 |
36 | {schemes.map(scheme => ( 37 | 38 | ))} 39 |
40 | ) 41 | 42 | CompactSwatches.propTypes = { 43 | schemes: PropTypes.arrayOf(PropTypes.string).isRequired, 44 | updateSwatch: PropTypes.func.isRequired 45 | } 46 | 47 | export default CompactSwatches 48 | -------------------------------------------------------------------------------- /src/components/Container.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { css } from 'emotion' 3 | import PropTypes from 'prop-types' 4 | 5 | // Main color picker container 6 | const Container = ({ children, width, background }) => ( 7 |
17 | {children} 18 |
19 | ) 20 | 21 | Container.defaultProps = { 22 | width: '222px', 23 | background: 'rgb(255, 255, 255)' 24 | } 25 | 26 | Container.propTypes = { 27 | children: PropTypes.oneOfType([ 28 | PropTypes.arrayOf(PropTypes.node), 29 | PropTypes.node 30 | ]).isRequired, 31 | width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 32 | background: PropTypes.string 33 | } 34 | 35 | export default Container 36 | -------------------------------------------------------------------------------- /src/components/Image.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const Image = ({ src, ...rest }) => ( 5 | 6 | ) 7 | 8 | Image.propTypes = { 9 | src: PropTypes.string.isRequired 10 | } 11 | 12 | export default Image 13 | -------------------------------------------------------------------------------- /src/components/Slider.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { css } from 'emotion' 3 | import PropTypes from 'prop-types' 4 | 5 | const Slider = ({ min, max, value, onChange, color, ...rest }) => ( 6 | 35 | ) 36 | 37 | Slider.defaultProps = { 38 | value: 0, 39 | onChange: () => {} 40 | } 41 | 42 | Slider.propTypes = { 43 | min: PropTypes.string.isRequired, 44 | max: PropTypes.string.isRequired, 45 | value: PropTypes.number, 46 | onChange: PropTypes.func, 47 | color: PropTypes.string.isRequired 48 | } 49 | 50 | export default Slider 51 | -------------------------------------------------------------------------------- /src/components/Swatch.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { css } from 'emotion' 3 | import PropTypes from 'prop-types' 4 | 5 | const Swatch = ({ 6 | color, 7 | updateSwatch, 8 | onSwatchHover, 9 | updateSwatchOnKeyDown 10 | }) => ( 11 |
35 | ) 36 | 37 | Swatch.defaultProps = { 38 | updateSwatch: () => {}, 39 | onSwatchHover: () => {}, 40 | updateSwatchOnKeyDown: () => {} 41 | } 42 | 43 | Swatch.propTypes = { 44 | color: PropTypes.string.isRequired, 45 | updateSwatch: PropTypes.func, 46 | onSwatchHover: PropTypes.func, 47 | updateSwatchOnKeyDown: PropTypes.func 48 | } 49 | 50 | export default Swatch 51 | -------------------------------------------------------------------------------- /src/components/Swatches.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { css } from 'emotion' 3 | import PropTypes from 'prop-types' 4 | 5 | import Swatch from './Swatch' 6 | 7 | const ENTER_KEY = 13 8 | 9 | const Swatches = ({ swatches, updateSwatch, onSwatchHover }) => ( 10 |
17 | {swatches.map(color => ( 18 | updateSwatch(color)} 22 | updateSwatchOnKeyDown={e => 23 | /* eslint-disable no-unused-vars */ 24 | e.keyCode === ENTER_KEY ? updateSwatch(color) : null 25 | } 26 | onSwatchHover={() => onSwatchHover && onSwatchHover(color)} 27 | /> 28 | ))} 29 |
30 | ) 31 | 32 | Swatches.defaultProps = { 33 | updateSwatch: () => {}, 34 | onSwatchHover: () => {} 35 | } 36 | 37 | Swatches.propTypes = { 38 | swatches: PropTypes.arrayOf(PropTypes.string).isRequired, 39 | updateSwatch: PropTypes.func, 40 | onSwatchHover: PropTypes.func 41 | } 42 | 43 | export default Swatches 44 | -------------------------------------------------------------------------------- /src/components/Tools.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled from 'react-emotion' 4 | 5 | import Slider from './Slider' 6 | import { Consumer } from '../utils/context' 7 | import { 8 | SaturatorIcon, 9 | DesaturatorIcon, 10 | ColorSpinIcon, 11 | ColorDarkenerIcon, 12 | ColorBrightenerIcon, 13 | ImagePickerIcon, 14 | SwatchesGeneratorIcon, 15 | ShadesGeneratorIcon, 16 | ResetIcon, 17 | TintsGeneratorIcon, 18 | ClipboardIcon, 19 | GenerateGradientIcon 20 | } from '../icons/index' 21 | 22 | // Copied from primer/primer-tooltips/build 23 | import '../styles/tooltip.css' 24 | 25 | const StyledSpan = styled('span')` 26 | outline: none; 27 | ` 28 | 29 | const StyledLabel = styled('label')` 30 | display: inline-block; 31 | width: 10px; 32 | position: relative; 33 | top: 3px; 34 | left: 4px; 35 | color: ${props => props.color}; 36 | ` 37 | 38 | const ENTER_KEY = 13 39 | 40 | /* eslint-disable operator-linebreak */ 41 | const TOOLTIP_CLASSNAME = 42 | 'tooltipped tooltipped-ne tooltipped-align-left-1 tooltipped-no-delay border p-2 mb-2 mr-2 float-left' 43 | 44 | const HOC = Comp => props => ( 45 | {color => } 46 | ) 47 | 48 | const ColorSaturator = HOC(({ color, value, onChange }) => ( 49 | 50 | 51 | 58 | {value} 59 | 60 | )) 61 | 62 | ColorSaturator.displayName = 'ColorSaturator' 63 | 64 | const ColorDesaturator = HOC(({ color, value, onChange }) => ( 65 | 66 | 67 | 74 | {value} 75 | 76 | )) 77 | 78 | ColorDesaturator.displayName = 'ColorDesaturator' 79 | 80 | const ColorBrightener = HOC(({ color, value, onChange }) => ( 81 | 82 | 83 | 90 | {value} 91 | 92 | )) 93 | 94 | ColorBrightener.displayName = 'ColorBrightener' 95 | 96 | const ColorDarkener = HOC(({ color, value, onChange }) => ( 97 | 98 | 99 | 106 | {value} 107 | 108 | )) 109 | 110 | ColorDarkener.displayName = 'ColorDarkener' 111 | 112 | const ColorSpinner = HOC(({ color, onChange, value }) => ( 113 | 114 | 115 | 122 | 123 | {value} 124 | ° 125 | 126 | 127 | )) 128 | 129 | ColorSpinner.displayName = 'ColorSpinner' 130 | 131 | const ImagePicker = HOC(({ color, uploadImage }) => ( 132 | 133 | 140 | 141 | 142 | )) 143 | 144 | ImagePicker.propTypes = { 145 | color: PropTypes.string, 146 | uploadImage: PropTypes.func 147 | } 148 | 149 | const PaletteGenerator = HOC(({ color, generateSwatches }) => ( 150 | (e.keyCode === ENTER_KEY ? generateSwatches(e) : null)} 155 | > 156 | 157 | 158 | )) 159 | 160 | PaletteGenerator.displayName = 'SwatchesGenerator' 161 | 162 | PaletteGenerator.propTypes = { 163 | color: PropTypes.string, 164 | generateSwatches: PropTypes.func 165 | } 166 | 167 | const ShadesGenerator = HOC(({ color, generateShades }) => ( 168 | (e.keyCode === ENTER_KEY ? generateShades(e) : null)} 173 | > 174 | 175 | 176 | )) 177 | 178 | ShadesGenerator.displayName = 'ShadesGenerator' 179 | 180 | ShadesGenerator.propTypes = { 181 | color: PropTypes.string, 182 | generateShades: PropTypes.func 183 | } 184 | 185 | const Reset = HOC(({ color, resetColors }) => ( 186 | (e.keyCode === ENTER_KEY ? resetColors(e) : null)} 191 | > 192 | 193 | 194 | )) 195 | 196 | Reset.displayName = 'Reset' 197 | 198 | Reset.propTypes = { 199 | color: PropTypes.string, 200 | resetColors: PropTypes.func 201 | } 202 | 203 | const TintsGenerator = HOC(({ color, generateTints }) => ( 204 | (e.keyCode === ENTER_KEY ? generateTints(e) : null)} 209 | > 210 | 211 | 212 | )) 213 | 214 | TintsGenerator.displayName = 'TintsGenerator' 215 | 216 | TintsGenerator.propTypes = { 217 | color: PropTypes.string, 218 | generateTints: PropTypes.func 219 | } 220 | 221 | const Clipboard = HOC(({ color, copyColor, showMsg, id = 'clipboard' }) => ( 222 | (e.keyCode === ENTER_KEY ? copyColor(e) : null)} 227 | className={showMsg ? TOOLTIP_CLASSNAME : 'no-tooltip'} 228 | aria-label="Copied" 229 | > 230 | 231 | 232 | )) 233 | 234 | Clipboard.propTypes = { 235 | color: PropTypes.string, 236 | copyColor: PropTypes.func, 237 | showMsg: PropTypes.bool, 238 | id: PropTypes.string 239 | } 240 | 241 | const GradientGenerator = HOC(({ color, generateGradient }) => ( 242 | (e.keyCode === ENTER_KEY ? generateGradient(e) : null)} 247 | > 248 | 249 | 250 | )) 251 | 252 | GradientGenerator.propTypes = { 253 | color: PropTypes.string, 254 | generateGradient: PropTypes.func 255 | } 256 | 257 | const AdvanceTools = { 258 | ColorSaturator, 259 | ColorDesaturator, 260 | ColorBrightener, 261 | ColorDarkener, 262 | ColorSpinner 263 | } 264 | 265 | Object.keys(AdvanceTools).forEach(tool => { 266 | AdvanceTools[tool].propTypes = { 267 | color: PropTypes.string, 268 | value: PropTypes.number, 269 | onChange: PropTypes.func 270 | } 271 | }) 272 | 273 | const BasicTools = { 274 | TintsGenerator, 275 | ShadesGenerator, 276 | Reset, 277 | ImagePicker, 278 | PaletteGenerator, 279 | Clipboard, 280 | GradientGenerator 281 | } 282 | 283 | export { AdvanceTools, BasicTools } 284 | -------------------------------------------------------------------------------- /src/components/Triangle.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { css } from 'emotion' 3 | import PropTypes from 'prop-types' 4 | 5 | const Triangle = ({ color }) => ( 6 |
19 | ) 20 | 21 | Triangle.propTypes = { 22 | color: PropTypes.string.isRequired 23 | } 24 | 25 | export default Triangle 26 | -------------------------------------------------------------------------------- /src/icons/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'react-emotion' 3 | 4 | const StyledSVG = styled('svg')` 5 | display: inline-block; 6 | width: 20px; 7 | position: relative; 8 | top: 8px; 9 | left: -5px; 10 | ` 11 | 12 | const ToolsSVG = styled('svg')` 13 | cursor: pointer; 14 | ` 15 | 16 | export const createStyledSVG = ({ 17 | height, 18 | width, 19 | viewBox, 20 | color, 21 | title, 22 | d, 23 | ...rest 24 | }) => ( 25 | 26 | {title} 27 | 28 | 29 | ) 30 | 31 | export const createToolsSVG = ({ 32 | height, 33 | width, 34 | viewBox, 35 | color, 36 | title, 37 | d, 38 | ...rest 39 | }) => ( 40 | 41 | {title} 42 | 43 | 44 | ) 45 | 46 | export const SaturatorIcon = ({ width, height, color }) => 47 | createStyledSVG({ 48 | height, 49 | width, 50 | color, 51 | title: 'color saturator', 52 | viewBox: '0 0 24 24', 53 | d: 54 | 'M12 15.422l3.75 2.25-0.984-4.266 3.328-2.906-4.406-0.375-1.688-4.031v9.328zM21.984 9.234l-5.438 4.734 1.641 7.031-6.188-3.75-6.188 3.75 1.641-7.031-5.438-4.734 7.172-0.609 2.813-6.609 2.813 6.609z' 55 | }) 56 | 57 | export const DesaturatorIcon = ({ width, height, color }) => 58 | createStyledSVG({ 59 | height, 60 | width, 61 | color, 62 | title: 'color desaturator', 63 | viewBox: '0 0 26 28', 64 | d: 65 | 'M23.859 22.625c1.172 1.859 0.344 3.375-1.859 3.375h-18c-2.203 0-3.031-1.516-1.859-3.375l7.859-12.391v-6.234h-1c-0.547 0-1-0.453-1-1s0.453-1 1-1h8c0.547 0 1 0.453 1 1s-0.453 1-1 1h-1v6.234zM11.688 11.297l-4.25 6.703h11.125l-4.25-6.703-0.313-0.484v-6.813h-2v6.813z' 66 | }) 67 | 68 | export const ColorSpinIcon = ({ width, height, color }) => 69 | createStyledSVG({ 70 | height, 71 | width, 72 | color, 73 | title: 'color spin', 74 | viewBox: '0 0 24 28', 75 | d: 76 | 'M23.609 16.5c0 0.031 0 0.078-0.016 0.109-1.328 5.531-5.891 9.391-11.656 9.391-3.047 0-6-1.203-8.219-3.313l-2.016 2.016c-0.187 0.187-0.438 0.297-0.703 0.297-0.547 0-1-0.453-1-1v-7c0-0.547 0.453-1 1-1h7c0.547 0 1 0.453 1 1 0 0.266-0.109 0.516-0.297 0.703l-2.141 2.141c1.469 1.375 3.422 2.156 5.437 2.156 2.781 0 5.359-1.437 6.813-3.813 0.375-0.609 0.562-1.203 0.828-1.828 0.078-0.219 0.234-0.359 0.469-0.359h3c0.281 0 0.5 0.234 0.5 0.5zM24 4v7c0 0.547-0.453 1-1 1h-7c-0.547 0-1-0.453-1-1 0-0.266 0.109-0.516 0.297-0.703l2.156-2.156c-1.484-1.375-3.437-2.141-5.453-2.141-2.781 0-5.359 1.437-6.813 3.813-0.375 0.609-0.562 1.203-0.828 1.828-0.078 0.219-0.234 0.359-0.469 0.359h-3.109c-0.281 0-0.5-0.234-0.5-0.5v-0.109c1.344-5.547 5.953-9.391 11.719-9.391 3.063 0 6.047 1.219 8.266 3.313l2.031-2.016c0.187-0.187 0.438-0.297 0.703-0.297 0.547 0 1 0.453 1 1z' 77 | }) 78 | 79 | export const ColorDarkenerIcon = ({ width, height, color }) => 80 | createStyledSVG({ 81 | height, 82 | width, 83 | color, 84 | title: 'color darkener', 85 | viewBox: '0 0 32 32', 86 | d: 87 | 'M24.633 22.184c-8.188 0-14.82-6.637-14.82-14.82 0-2.695 0.773-5.188 2.031-7.363-6.824 1.968-11.844 8.187-11.844 15.644 0 9.031 7.32 16.355 16.352 16.355 7.457 0 13.68-5.023 15.648-11.844-2.18 1.254-4.672 2.028-7.367 2.028z' 88 | }) 89 | 90 | export const ColorBrightenerIcon = ({ width, height, color }) => 91 | createStyledSVG({ 92 | height, 93 | width, 94 | color, 95 | title: 'color brightener', 96 | viewBox: '0 0 32 32', 97 | d: 98 | 'M16 9c-3.859 0-7 3.141-7 7s3.141 7 7 7 7-3.141 7-7c0-3.859-3.141-7-7-7zM16 21c-2.762 0-5-2.238-5-5s2.238-5 5-5 5 2.238 5 5-2.238 5-5 5zM16 7c0.552 0 1-0.448 1-1v-2c0-0.552-0.448-1-1-1s-1 0.448-1 1v2c0 0.552 0.448 1 1 1zM16 25c-0.552 0-1 0.448-1 1v2c0 0.552 0.448 1 1 1s1-0.448 1-1v-2c0-0.552-0.448-1-1-1zM23.777 9.635l1.414-1.414c0.391-0.391 0.391-1.023 0-1.414s-1.023-0.391-1.414 0l-1.414 1.414c-0.391 0.391-0.391 1.023 0 1.414s1.023 0.391 1.414 0zM8.223 22.365l-1.414 1.414c-0.391 0.391-0.391 1.023 0 1.414s1.023 0.391 1.414 0l1.414-1.414c0.391-0.392 0.391-1.023 0-1.414s-1.023-0.392-1.414 0zM7 16c0-0.552-0.448-1-1-1h-2c-0.552 0-1 0.448-1 1s0.448 1 1 1h2c0.552 0 1-0.448 1-1zM28 15h-2c-0.552 0-1 0.448-1 1s0.448 1 1 1h2c0.552 0 1-0.448 1-1s-0.448-1-1-1zM8.221 9.635c0.391 0.391 1.024 0.391 1.414 0s0.391-1.023 0-1.414l-1.414-1.414c-0.391-0.391-1.023-0.391-1.414 0s-0.391 1.023 0 1.414l1.414 1.414zM23.779 22.363c-0.392-0.391-1.023-0.391-1.414 0s-0.392 1.023 0 1.414l1.414 1.414c0.391 0.391 1.023 0.391 1.414 0s0.391-1.023 0-1.414l-1.414-1.414z' 99 | }) 100 | 101 | export const ImagePickerIcon = ({ width, height, color, id }) => 102 | createToolsSVG({ 103 | height, 104 | width, 105 | id, 106 | color, 107 | title: 'image picker', 108 | viewBox: '0 0 30 28', 109 | d: 110 | 'M10 9c0 1.656-1.344 3-3 3s-3-1.344-3-3 1.344-3 3-3 3 1.344 3 3zM26 15v7h-22v-3l5-5 2.5 2.5 8-8zM27.5 4h-25c-0.266 0-0.5 0.234-0.5 0.5v19c0 0.266 0.234 0.5 0.5 0.5h25c0.266 0 0.5-0.234 0.5-0.5v-19c0-0.266-0.234-0.5-0.5-0.5zM30 4.5v19c0 1.375-1.125 2.5-2.5 2.5h-25c-1.375 0-2.5-1.125-2.5-2.5v-19c0-1.375 1.125-2.5 2.5-2.5h25c1.375 0 2.5 1.125 2.5 2.5z' 111 | }) 112 | 113 | export const ShadesGeneratorIcon = ({ width, height, color }) => 114 | createToolsSVG({ 115 | height, 116 | width, 117 | color, 118 | title: 'shades generator', 119 | viewBox: '0 0 24 28', 120 | d: 121 | 'M12 22.5v-17c-4.688 0-8.5 3.813-8.5 8.5s3.813 8.5 8.5 8.5zM24 14c0 6.625-5.375 12-12 12s-12-5.375-12-12 5.375-12 12-12 12 5.375 12 12z' 122 | }) 123 | 124 | export const TintsGeneratorIcon = ({ width, height, color }) => 125 | createToolsSVG({ 126 | height, 127 | width, 128 | color, 129 | title: 'tints generator', 130 | viewBox: '0 0 16 28', 131 | d: 132 | 'M8 18c0-0.391-0.125-0.766-0.313-1.078-0.203-0.313-1.031-1.375-1.359-2.422-0.047-0.172-0.203-0.25-0.328-0.25s-0.281 0.078-0.328 0.25c-0.328 1.047-1.156 2.109-1.359 2.422-0.187 0.313-0.313 0.688-0.313 1.078 0 1.109 0.891 2 2 2s2-0.891 2-2zM16 16c0 4.422-3.578 8-8 8s-8-3.578-8-8c0-1.578 0.484-3.047 1.266-4.297 0.797-1.25 4.141-5.484 5.406-9.703 0.203-0.672 0.828-1 1.328-1s1.141 0.328 1.328 1c1.266 4.219 4.609 8.453 5.406 9.703s1.266 2.719 1.266 4.297z' 133 | }) 134 | 135 | export const ClipboardIcon = ({ width, height, color }) => 136 | createToolsSVG({ 137 | height, 138 | width, 139 | color, 140 | title: 'clipboard', 141 | viewBox: '0 0 20 20', 142 | d: 143 | 'M15.6 2l-1.2 3h-8.8l-1.2-3c-0.771 0-1.4 0.629-1.4 1.4v15.2c0 0.77 0.629 1.4 1.399 1.4h11.2c0.77 0 1.4-0.631 1.4-1.4v-15.2c0.001-0.771-0.63-1.4-1.399-1.4zM13.6 4l0.9-2h-2.181l-0.719-2h-3.2l-0.72 2h-2.18l0.899 2h7.201z' 144 | }) 145 | 146 | export const SwatchesGeneratorIcon = ({ width, height, color }) => 147 | createToolsSVG({ 148 | height, 149 | width, 150 | color, 151 | title: 'swatches generator', 152 | viewBox: '0 0 24 24', 153 | d: 154 | 'M17.484 12c0.844 0 1.5-0.656 1.5-1.5s-0.656-1.5-1.5-1.5-1.5 0.656-1.5 1.5 0.656 1.5 1.5 1.5zM14.484 8.016c0.844 0 1.5-0.656 1.5-1.5s-0.656-1.5-1.5-1.5-1.5 0.656-1.5 1.5 0.656 1.5 1.5 1.5zM9.516 8.016c0.844 0 1.5-0.656 1.5-1.5s-0.656-1.5-1.5-1.5-1.5 0.656-1.5 1.5 0.656 1.5 1.5 1.5zM6.516 12c0.844 0 1.5-0.656 1.5-1.5s-0.656-1.5-1.5-1.5-1.5 0.656-1.5 1.5 0.656 1.5 1.5 1.5zM12 3c4.969 0 9 3.609 9 8.016 0 2.766-2.25 4.969-5.016 4.969h-1.734c-0.844 0-1.5 0.656-1.5 1.5 0 0.375 0.141 0.703 0.375 0.984s0.375 0.656 0.375 1.031c0 0.844-0.656 1.5-1.5 1.5-4.969 0-9-4.031-9-9s4.031-9 9-9z' 155 | }) 156 | 157 | export const ResetIcon = ({ width, height, color }) => 158 | createToolsSVG({ 159 | height, 160 | width, 161 | color, 162 | title: 'reset state', 163 | viewBox: '0 0 32 32', 164 | d: 165 | 'M32 16c0-8.836-7.164-16-16-16-8.837 0-16 7.164-16 16 0 8.837 7.163 16 16 16 8.836 0 16-7.163 16-16zM8 16l8-8v6h8v4h-8v6l-8-8z' 166 | }) 167 | 168 | export const GenerateGradientIcon = ({ width, height, color }) => 169 | createToolsSVG({ 170 | height, 171 | width, 172 | color, 173 | title: 'gradient generator', 174 | viewBox: '0 0 32 32', 175 | d: 176 | 'M32 12h-12l4.485-4.485c-2.267-2.266-5.28-3.515-8.485-3.515s-6.219 1.248-8.485 3.515c-2.266 2.267-3.515 5.28-3.515 8.485s1.248 6.219 3.515 8.485c2.267 2.266 5.28 3.515 8.485 3.515s6.219-1.248 8.485-3.515c0.189-0.189 0.371-0.384 0.546-0.583l3.010 2.634c-2.933 3.349-7.239 5.464-12.041 5.464-8.837 0-16-7.163-16-16s7.163-16 16-16c4.418 0 8.418 1.791 11.313 4.687l4.687-4.687v12z' 177 | }) 178 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import BasicPicker from './pickers/BasicPicker' 2 | import GradientPicker from './pickers/GradientPicker' 3 | import SchemePicker from './pickers/SchemePicker' 4 | import { utils } from './utils/colors' 5 | 6 | export { BasicPicker, GradientPicker, SchemePicker, utils } 7 | -------------------------------------------------------------------------------- /src/pickers/BasicPicker.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ColorExtractor } from 'react-color-extractor' 3 | import generateColors from 'randomcolor' 4 | import { TinyColor } from '@ctrl/tinycolor' 5 | import Values from 'values.js' 6 | import PropTypes from 'prop-types' 7 | import styled from 'react-emotion' 8 | import clipboard from 'clipboard-polyfill' 9 | 10 | import ColorInput from '../components/ColorInputField' 11 | import ColorBlock from '../components/ColorBlock' 12 | import Container from '../components/Container' 13 | import Image from '../components/Image' 14 | import Swatches from '../components/Swatches' 15 | import Triangle from '../components/Triangle' 16 | import ColorFormatPicker from '../components/ColorFormatPicker' 17 | import { AdvanceTools, BasicTools } from '../components/Tools' 18 | 19 | import { Provider as ColorProvider } from '../utils/context' 20 | import { 21 | DEFAULT_SWATCHES, 22 | DEFAULT_COLOR, 23 | COLOR_CONTAINER_WIDTH, 24 | MAX_COLORS 25 | } from '../utils/constants' 26 | import getThemeVariants from '../utils/theme' 27 | 28 | const StyledList = styled('ul')` 29 | display: grid; 30 | justify-content: center; 31 | grid-template-columns: 1fr; 32 | grid-gap: 15px; 33 | list-style: none; 34 | margin-left: -28px; 35 | ` 36 | 37 | const ToolsContainer = styled('div')` 38 | display: grid; 39 | grid-template-columns: ${props => 40 | `repeat(${props.columns || 6}, ${props.size || '1fr'})`}; 41 | grid-gap: ${props => props.gap || '5px'}; 42 | margin-right: -20px; 43 | margin-top: 20px; 44 | ` 45 | 46 | export default class BasicPicker extends React.PureComponent { 47 | // Image upload icon 48 | imageIcon = null 49 | 50 | // Hidden input element for uploading an image 51 | uploadElement = null 52 | 53 | // Clipboard icon 54 | clipboardIcon = null 55 | 56 | state = { 57 | // Current block, active and input field color (or hue) 58 | color: new TinyColor(this.props.color), 59 | // Current swatches to be displayed in picker 60 | swatches: this.props.swatches, 61 | // Image from which colors are extracted 62 | image: null, 63 | // Shades are the hue darkened with black 64 | shades: [], 65 | // Tints are the hue lightend with white 66 | tints: [], 67 | // Should display the shades swatches 68 | showShades: false, 69 | // Should display the tint swatches 70 | showTints: false, 71 | // Current color format selected 72 | currentFormat: 'HEX', 73 | // Color format options 74 | formats: ['HEX', 'HSV', 'RGB', 'HSL'], 75 | // Color manipulation values 76 | // Brightens the currently selected color by an amount 77 | brighten: 0, 78 | // Darkens the currently selected color by an amount 79 | darken: 0, 80 | // spin operation spins (changes) the current hue 81 | spin: 0, 82 | // desaturation makes a color more muted (with black or grey) 83 | desaturate: 0, 84 | // saturation controls the intensity (or purity) of a color 85 | saturate: 0, 86 | // Show or hide color copied msg 87 | showMsg: false 88 | } 89 | 90 | static defaultProps = { 91 | color: DEFAULT_COLOR, 92 | swatches: DEFAULT_SWATCHES, 93 | // Max amount of colors from which palettes will be generated (from the image) 94 | maxColors: MAX_COLORS, 95 | triangle: true, 96 | theme: 'light', 97 | // Color tools are disabled by default 98 | showTools: false 99 | } 100 | 101 | static propTypes = { 102 | color: PropTypes.string, 103 | /* eslint-disable react/require-default-props */ 104 | onChange: PropTypes.func, 105 | onSwatchHover: PropTypes.func, 106 | swatches: PropTypes.arrayOf(PropTypes.string), 107 | maxColors: PropTypes.number, 108 | triangle: PropTypes.bool, 109 | theme: PropTypes.oneOf(['light', 'dark']), 110 | showTools: PropTypes.bool 111 | } 112 | 113 | // Instance properties are used to store the color state on 114 | // which the color operations will be applied. 115 | brightenColor = null 116 | 117 | darkenColor = null 118 | 119 | spinColor = null 120 | 121 | desaturateColor = null 122 | 123 | saturateColor = null 124 | 125 | componentDidMount() { 126 | this.uploadElement = document.getElementById('uploader') 127 | this.imageIcon = document.getElementById('image-icon') 128 | this.clipboardIcon = document.getElementById('clipboard') 129 | 130 | this.imageIcon && 131 | this.imageIcon.addEventListener('click', this.simulateClick) 132 | this.clipboardIcon && 133 | this.clipboardIcon.addEventListener('mouseleave', this.hideMsg) 134 | this.clipboardIcon && 135 | this.clipboardIcon.addEventListener('blur', this.hideMsg) 136 | 137 | // Attach a listener for deleting the image (if any) from the color block 138 | document.addEventListener('keydown', this.updateKey) 139 | } 140 | 141 | componentDidUpdate(prevProps) { 142 | if (prevProps.color !== this.props.color) { 143 | const color = new TinyColor(this.props.color) 144 | // Check if its a valid hex and then update the color 145 | // on changing the color input field, it only updates the color block if the hex code is valid 146 | if (color.isValid) { 147 | this.setState({ color }) 148 | } 149 | } 150 | } 151 | 152 | componentWillUnmount() { 153 | this.imageIcon && 154 | this.imageIcon.removeEventListener('click', this.simulateClick) 155 | this.clipboardIcon && 156 | this.clipboardIcon.removeEventListener('mouseleave', this.hideMsg) 157 | this.clipboardIcon && 158 | this.clipboardIcon.removeEventListener('blur', this.hideMsg) 159 | document.removeEventListener('keydown', this.updateKey) 160 | } 161 | 162 | hideMsg = () => this.setState({ showMsg: false }) 163 | 164 | // default onChange handler for color input field 165 | defaultOnChange = color => { 166 | const newColor = new TinyColor(color) 167 | 168 | if (newColor.isValid) { 169 | this.setState({ color: newColor }) 170 | } 171 | } 172 | 173 | updateColorState = (value, color, operation) => { 174 | const newValue = parseInt(value) 175 | const newColor = new TinyColor(color)[operation](newValue) 176 | 177 | this.props.onChange && this.props.onChange(newColor.toHexString()) 178 | this.setState({ [operation]: newValue, color: newColor }) 179 | } 180 | 181 | clearAllColorBuffers = () => { 182 | this.spinColor = null 183 | this.saturateColor = null 184 | this.desaturateColor = null 185 | this.darkenColor = null 186 | this.brightenColor = null 187 | } 188 | 189 | /** 190 | * Below methods are used to handle color operations. Whenever an 191 | * operation is performed on a color, it mutates the original state 192 | * of the color. So we use instance properties to clear and set the 193 | * currently active color state, and then apply the color operations 194 | * w.r.t to the instance property (or current color value) 195 | * 196 | * TODO: Refactor this mess 197 | */ 198 | 199 | handleSpin = e => { 200 | if (this.spinColor === null) { 201 | this.spinColor = this.state.color.originalInput 202 | } 203 | 204 | this.saturateColor = null 205 | this.desaturateColor = null 206 | this.brightenColor = null 207 | this.darkenColor = null 208 | 209 | this.updateColorState(e.target.value, this.spinColor, 'spin') 210 | } 211 | 212 | handleSaturate = e => { 213 | if (this.saturateColor === null) { 214 | this.saturateColor = this.state.color.originalInput 215 | } 216 | 217 | this.spinColor = null 218 | this.desaturateColor = null 219 | this.brightenColor = null 220 | this.darkenColor = null 221 | 222 | this.updateColorState(e.target.value, this.saturateColor, 'saturate') 223 | } 224 | 225 | handleDesaturate = e => { 226 | if (this.desaturateColor === null) { 227 | this.desaturateColor = this.state.color.originalInput 228 | } 229 | 230 | this.saturateColor = null 231 | this.spinColor = null 232 | this.brightenColor = null 233 | this.darkenColor = null 234 | 235 | this.updateColorState(e.target.value, this.desaturateColor, 'desaturate') 236 | } 237 | 238 | handleBrighten = e => { 239 | if (this.brightenColor === null) { 240 | this.brightenColor = this.state.color.originalInput 241 | } 242 | 243 | this.saturateColor = null 244 | this.desaturateColor = null 245 | this.spinColor = null 246 | this.darkenColor = null 247 | 248 | this.updateColorState(e.target.value, this.brightenColor, 'brighten') 249 | } 250 | 251 | handleDarken = e => { 252 | if (this.darkenColor === null) { 253 | this.darkenColor = this.state.color.originalInput 254 | } 255 | 256 | this.saturateColor = null 257 | this.desaturateColor = null 258 | this.brightenColor = null 259 | this.spinColor = null 260 | 261 | this.updateColorState(e.target.value, this.darkenColor, 'darken') 262 | } 263 | 264 | // outputs the color according to the color format 265 | getColor = color => ({ 266 | HSL: color.toHslString(), 267 | HEX: color.toHexString(), 268 | RGB: color.toRgbString(), 269 | HSV: color.toHsvString() 270 | }) 271 | 272 | // This handler is used to update the image state. After the colors 273 | // are extracted from the image, a image can be removed from the color block. 274 | updateKey = e => { 275 | if (e.which === 8) { 276 | // Remove the image from color block 277 | this.setState({ image: null }) 278 | } 279 | } 280 | 281 | simulateClick = e => { 282 | if (this.uploadElement) { 283 | this.uploadElement.click() 284 | } 285 | 286 | e.preventDefault() 287 | } 288 | 289 | // Randomly generate new swatches 290 | generateSwatches = () => { 291 | let i = 0 292 | 293 | // Each swatch should be different 294 | const newColors = new Set() 295 | 296 | while (i < 12) { 297 | newColors.add(generateColors()) 298 | i += 1 299 | } 300 | 301 | const swatches = Array.from(newColors) 302 | 303 | // Hide shades and tints when new swatches are added 304 | this.setState({ 305 | swatches: [...swatches], 306 | showShades: false, 307 | showTints: false, 308 | tints: [], 309 | shades: [] 310 | }) 311 | } 312 | 313 | uploadImage = e => 314 | this.setState({ image: window.URL.createObjectURL(e.target.files[0]) }) 315 | 316 | // Updates the hue state 317 | updateSwatch = color => { 318 | // If tools are active, then reset color instance properties. 319 | if (this.props.showTools) { 320 | this.clearAllColorBuffers() 321 | 322 | this.setState({ 323 | color: new TinyColor(color), 324 | // Reset all the values for the newly selected swatch 325 | spin: 0, 326 | saturate: 0, 327 | desaturate: 0, 328 | darken: 0, 329 | brighten: 0 330 | }) 331 | } else { 332 | this.setState({ 333 | color: new TinyColor(color) 334 | }) 335 | } 336 | 337 | this.props.onChange && this.props.onChange(color) 338 | } 339 | 340 | // Handler to update swatches when colors are extracted from an image 341 | updateSwatches = swatches => 342 | this.setState({ 343 | swatches: [...swatches], 344 | // Also update the current color 345 | color: new TinyColor(swatches[0]), 346 | // Hide the shades and tints 347 | showShades: false, 348 | showTints: false, 349 | tints: [], 350 | shades: [] 351 | }) 352 | 353 | // Generates shades or tints from the currently selected hue (color) 354 | generateSwatchesFromHue = (term, showShades, showTints) => { 355 | const colorBuffer = [] 356 | const color = new Values(this.state.color.toHexString()) 357 | 358 | color[term]().forEach(c => colorBuffer.push(c.hexString())) 359 | 360 | this.setState({ 361 | [term]: [...colorBuffer], 362 | showShades, 363 | showTints 364 | }) 365 | } 366 | 367 | // Shades - A hue lightened with white 368 | generateShades = () => this.generateSwatchesFromHue('shades', true, false) 369 | 370 | // Tints - A hue darkened with black 371 | generateTints = () => this.generateSwatchesFromHue('tints', false, true) 372 | 373 | // Update the color format (hsv, rgb, hex, or hsl) 374 | changeFormat = e => this.setState({ currentFormat: e.target.value }) 375 | 376 | // Reset the shades and tints, and displays the previous swatches 377 | resetColors = () => 378 | this.setState({ 379 | shades: [], 380 | tints: [], 381 | showShades: false, 382 | showTints: false 383 | }) 384 | 385 | copyColor = () => { 386 | const { color, currentFormat } = this.state 387 | const activeColor = this.getColor(color)[currentFormat] 388 | 389 | clipboard.writeText(activeColor) 390 | this.setState({ showMsg: true }) 391 | } 392 | 393 | render() { 394 | const { 395 | image, 396 | swatches, 397 | shades, 398 | showShades, 399 | showTints, 400 | tints, 401 | currentFormat, 402 | darken, 403 | brighten, 404 | spin, 405 | desaturate, 406 | saturate, 407 | formats 408 | } = this.state 409 | // Get the color string with a specified color format 410 | const color = this.getColor(this.state.color)[currentFormat] 411 | const { bg, iconColor } = getThemeVariants(this.props.theme) 412 | 413 | return ( 414 | 415 | {/* eslint-disable operator-linebreak */} 416 | {/* eslint-disable indent */} 417 | {this.props.triangle && 418 | image === null && } 419 | {image === null ? ( 420 | 425 | ) : ( 426 | 427 | )} 428 | {image && ( 429 | 434 | )} 435 |
436 | {showShades ? ( 437 | 442 | ) : showTints ? ( 443 | 448 | ) : ( 449 | 454 | )} 455 | 459 | 463 | 464 | 465 | 466 | 469 | 470 | 473 | 477 | 478 | 479 | 480 | {this.props.showTools ? ( 481 |
482 | 483 | 484 |
  • 485 | 489 |
  • 490 |
  • 491 | 495 |
  • 496 |
  • 497 | 501 |
  • 502 |
  • 503 | 507 |
  • 508 |
  • 509 | 513 |
  • 514 |
    515 |
    516 |
    517 | ) : null} 518 |
    519 |
    520 | ) 521 | } 522 | } 523 | -------------------------------------------------------------------------------- /src/pickers/GradientPicker.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import gradient from 'tinygradient' 3 | import PropTypes from 'prop-types' 4 | import { TinyColor } from '@ctrl/tinycolor' 5 | import styled, { css } from 'react-emotion' 6 | import clipboard from 'clipboard-polyfill' 7 | 8 | import Container from '../components/Container' 9 | import ColorInputField from '../components/ColorInputField' 10 | import Slider from '../components/Slider' 11 | import { BasicTools } from '../components/Tools' 12 | 13 | import { Provider as ColorProvider, Consumer } from '../utils/context' 14 | import { 15 | DEFAULT_COLOR_ONE, 16 | DEFAULT_COLOR_TWO, 17 | GRADIENT_CONTAINER_WIDTH, 18 | GRADIENT_CONTAINER_HEIGHT, 19 | DEFAULT_COLOR_STOP 20 | } from '../utils/constants' 21 | import { randomColors } from '../utils/colors' 22 | import getThemeVariants from '../utils/theme' 23 | 24 | const StyledLabel = styled('label')` 25 | display: inline-block; 26 | width: 80px; 27 | position: relative; 28 | top: 3px; 29 | left: 4px; 30 | font-size: 14px; 31 | margin-bottom: 5px; 32 | color: ${props => props.color}; 33 | ` 34 | 35 | // Colors should be sorted by the color stops position values 36 | const createGradient = colors => 37 | gradient( 38 | colors.sort((a, b) => { 39 | // We need to shift the value of either color stop because tinygradient 40 | // throws an error when two stops are equal. 41 | if (a.pos && b.pos && a.pos === b.pos) { 42 | /* eslint-disable no-param-reassign */ 43 | a.pos += 0.01 44 | } 45 | 46 | return a.pos - b.pos 47 | }) 48 | ) 49 | 50 | const ColorBlock = ({ gradient: gradientCss }) => ( 51 |
    62 | ) 63 | 64 | ColorBlock.propTypes = { 65 | gradient: PropTypes.string.isRequired 66 | } 67 | 68 | const ColorStop = ({ 69 | color: inputColor, 70 | onChangeColor, 71 | value, 72 | onChangeStop 73 | }) => ( 74 | 75 | {color => ( 76 |
    77 | 78 | 79 | 80 | Stops 81 | 82 | 91 | 92 |
    93 | )} 94 |
    95 | ) 96 | 97 | ColorStop.propTypes = { 98 | color: PropTypes.string.isRequired, 99 | onChangeColor: PropTypes.func.isRequired, 100 | value: PropTypes.number.isRequired, 101 | onChangeStop: PropTypes.func.isRequired 102 | } 103 | 104 | export default class GradientPicker extends React.Component { 105 | // Clipboard icon element 106 | clipboardIcon = null 107 | 108 | state = { 109 | // Returns a gradient object 110 | gradient: this.props.reverse 111 | ? gradient(this.props.colorOne, this.props.colorTwo).reverse() 112 | : gradient(this.props.colorOne, this.props.colorTwo), 113 | // Default colors for creating a gradient 114 | colorOne: this.props.colorOne, 115 | colorTwo: this.props.colorTwo, 116 | // Color stops are stopping points in a gradient that show a specific color 117 | // at the exact location we set. 118 | // Stop loc. for color one 119 | colorStopOne: 0, 120 | // Stop loc. for color two 121 | colorStopTwo: 0, 122 | // Show copy msg 123 | showMsg: false 124 | } 125 | 126 | static defaultProps = { 127 | colorOne: DEFAULT_COLOR_ONE, 128 | colorTwo: DEFAULT_COLOR_TWO, 129 | // When set to true, reverse the gradient 130 | reverse: false, 131 | // Returns a css gradient string. It is invoked on every operation like 132 | // (setting stop values, or updating the color input field) 133 | /* eslint-disable no-unused-vars */ 134 | getGradient: grad => {}, 135 | theme: 'light' 136 | // These defaults are built-in in tinygradient module 137 | // mode: 'linear', 138 | // direction: 'to bottom' 139 | } 140 | 141 | static propTypes = { 142 | colorOne: PropTypes.string, 143 | colorTwo: PropTypes.string, 144 | getGradient: PropTypes.func, 145 | theme: PropTypes.oneOf(['light', 'dark']), 146 | /* eslint-disable react/require-default-props */ 147 | mode: PropTypes.oneOf(['linear', 'radial']), 148 | direction: PropTypes.string, 149 | reverse: PropTypes.bool 150 | } 151 | 152 | componentDidMount() { 153 | this.propCallback() 154 | 155 | this.clipboardIcon = document.getElementById('gradient-clipboard') 156 | 157 | this.clipboardIcon && 158 | this.clipboardIcon.addEventListener('mouseleave', this.hideMsg) 159 | this.clipboardIcon && 160 | this.clipboardIcon.addEventListener('blur', this.hideMsg) 161 | } 162 | 163 | componentWillUnmount() { 164 | this.clipboardIcon && 165 | this.clipboardIcon.removeEventListener('mouseleave', this.hideMsg) 166 | this.clipboardIcon && 167 | this.clipboardIcon.removeEventListener('blur', this.hideMsg) 168 | } 169 | 170 | hideMsg = () => this.setState({ showMsg: false }) 171 | 172 | setColorStopOne = pos => { 173 | // Only set the stop property if it's non-zero 174 | /* eslint-disable operator-linebreak */ 175 | 176 | const colorOne = 177 | pos !== 0 ? { color: this.state.colorOne, pos } : this.state.colorOne 178 | const colorTwo = 179 | pos !== 0 180 | ? { color: this.state.colorTwo, pos: DEFAULT_COLOR_STOP } 181 | : this.state.colorTwo 182 | 183 | return { 184 | colorOne, 185 | colorTwo 186 | } 187 | } 188 | 189 | setColorStopTwo = pos => { 190 | /* eslint-disable operator-linebreak */ 191 | const colorOne = 192 | pos !== 0 193 | ? { color: this.state.colorOne, pos: DEFAULT_COLOR_STOP } 194 | : this.state.colorOne 195 | 196 | const colorTwo = 197 | pos !== 0 ? { color: this.state.colorTwo, pos } : this.state.colorTwo 198 | 199 | return { 200 | colorOne, 201 | colorTwo 202 | } 203 | } 204 | 205 | /* eslint-disable indent */ 206 | propCallback = () => 207 | this.props.reverse 208 | ? this.props.getGradient( 209 | this.state.gradient 210 | .reverse() 211 | .css(this.props.mode, this.props.direction) 212 | ) 213 | : this.props.getGradient( 214 | this.state.gradient.css(this.props.mode, this.props.direction) 215 | ) 216 | 217 | updateColorStop = (e, color) => { 218 | const value = parseInt(e.target.value) 219 | // color stop position value should be between 0 and 1 220 | const pos = value / 10 221 | 222 | // Create the gradient depending on the color stop value and color state 223 | if (color === 'colorStopOne') { 224 | const { colorOne, colorTwo } = this.setColorStopOne(pos) 225 | 226 | this.setState( 227 | { gradient: createGradient([colorOne, colorTwo]), [color]: value }, 228 | this.propCallback 229 | ) 230 | } else if (color === 'colorStopTwo') { 231 | const { colorOne, colorTwo } = this.setColorStopTwo(pos) 232 | 233 | this.setState( 234 | { gradient: createGradient([colorOne, colorTwo]), [color]: value }, 235 | this.propCallback 236 | ) 237 | } 238 | } 239 | 240 | // Create the gradient when the color stop value for color changes 241 | 242 | updateStopOne = e => this.updateColorStop(e, 'colorStopOne') 243 | 244 | updateStopTwo = e => this.updateColorStop(e, 'colorStopTwo') 245 | 246 | // Create the gradient when the either color changes 247 | 248 | updateColorOne = color => { 249 | this.setState({ colorOne: color }) 250 | 251 | const newColor = new TinyColor(color) 252 | 253 | if (newColor.isValid) { 254 | this.setState( 255 | state => ({ 256 | gradient: gradient(color, state.colorTwo) 257 | }), 258 | this.propCallback 259 | ) 260 | } 261 | } 262 | 263 | updateColorTwo = color => { 264 | this.setState({ colorTwo: color }) 265 | 266 | const newColor = new TinyColor(color) 267 | 268 | if (newColor.isValid) { 269 | this.setState( 270 | state => ({ 271 | gradient: gradient(state.colorOne, color) 272 | }), 273 | this.propCallback 274 | ) 275 | } 276 | } 277 | 278 | // Generate different color inputs and create a gradient using those input values 279 | generateGradient = () => { 280 | const iterator = randomColors().values() 281 | 282 | const colorOne = iterator.next().value 283 | const colorTwo = iterator.next().value 284 | 285 | this.setState( 286 | { colorOne, colorTwo, gradient: gradient(colorOne, colorTwo) }, 287 | this.propCallback 288 | ) 289 | } 290 | 291 | copyColor = () => { 292 | clipboard.writeText(this.state.gradient.css()) 293 | this.setState({ showMsg: true }) 294 | } 295 | 296 | render() { 297 | const { 298 | gradient: grad, 299 | colorOne, 300 | colorTwo, 301 | colorStopOne, 302 | colorStopTwo, 303 | showMsg 304 | } = this.state 305 | const { bg, iconColor } = getThemeVariants(this.props.theme) 306 | 307 | return ( 308 | 313 | 314 | 315 |
    316 | 322 | 328 |
    335 | 340 |
    341 | 344 |
    345 |
    346 |
    347 |
    348 |
    349 | ) 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /src/pickers/SchemePicker.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { TinyColor } from '@ctrl/tinycolor' 3 | import { css } from 'emotion' 4 | import PropTypes from 'prop-types' 5 | import clipboard from 'clipboard-polyfill' 6 | 7 | import Container from '../components/Container' 8 | import ColorBlock from '../components/ColorBlock' 9 | import ColorInputField from '../components/ColorInputField' 10 | import { BasicTools } from '../components/Tools' 11 | import CompactSwatches from '../components/CompactSwatches' 12 | 13 | import { Provider as ColorProvider } from '../utils/context' 14 | import { 15 | SCHEME_CONTAINER_HEIGHT, 16 | SCHEME_CONTAINER_WIDTH 17 | } from '../utils/constants' 18 | import getThemeVariants from '../utils/theme' 19 | 20 | const MAX_COLOR_SCHEMES = 30 21 | 22 | export default class SchemePicker extends React.Component { 23 | clipboardIcon = null 24 | 25 | state = { 26 | color: new TinyColor(this.props.color).toHexString(), 27 | swatches: [], 28 | showMsg: false 29 | } 30 | 31 | static defaultProps = { 32 | color: 'hotpink', 33 | theme: 'light', 34 | // Default color scheme from which swatches are generated 35 | scheme: 'monochromatic' 36 | } 37 | 38 | static propTypes = { 39 | color: PropTypes.string, 40 | /* eslint-disable react/require-default-props */ 41 | onChange: PropTypes.func, 42 | theme: PropTypes.oneOf(['light', 'dark']), 43 | scheme: PropTypes.oneOf([ 44 | 'monochromatic', 45 | 'splitcomplement', 46 | 'triad', 47 | 'tetrad', 48 | 'analogous' 49 | ]) 50 | } 51 | 52 | componentDidMount() { 53 | this.generateSchemes(this.props.color) 54 | 55 | this.clipboardIcon = document.getElementById('scheme-picker-clipboard') 56 | 57 | this.clipboardIcon && 58 | this.clipboardIcon.addEventListener('mouseleave', this.hideMsg) 59 | this.clipboardIcon && 60 | this.clipboardIcon.addEventListener('blur', this.hideMsg) 61 | } 62 | 63 | componentDidUpdate(prevProps) { 64 | // Only invoked when the color input is updated 65 | /* eslint-disable operator-linebreak */ 66 | if ( 67 | this.props.color !== prevProps.color && 68 | /* eslint-disable max-len */ 69 | this.props.color !== this.state.color // This ensures that when we click on a swatch, it will not generate the swatches for the currently selected swatch. 70 | ) { 71 | const newColor = new TinyColor(this.props.color) 72 | 73 | if (newColor.isValid) { 74 | this.setState({ color: newColor.toHexString() }, () => 75 | this.generateSchemes(newColor) 76 | ) 77 | } 78 | } 79 | } 80 | 81 | componentWillUnmount() { 82 | this.clipboardIcon && 83 | this.clipboardIcon.removeEventListener('mouseleave', this.hideMsg) 84 | this.clipboardIcon && 85 | this.clipboardIcon.removeEventListener('blur', this.hideMsg) 86 | } 87 | 88 | hideMsg = () => this.setState({ showMsg: false }) 89 | 90 | // Generate new color schemes based on the color input and current format state 91 | generateSchemes = color => { 92 | const newSchemes = new TinyColor(color) 93 | [ 94 | /* eslint-disable no-unexpected-multiline */ 95 | this.props.scheme 96 | ](MAX_COLOR_SCHEMES) // the max color scheme amount will be adjusted by TinyColor for different color schemes 97 | /* eslint-disable max-len */ 98 | .map(c => c.toHexString()) // Get the hex string of each color 99 | .reverse() // We have to display the colors from light to dark, so reverse the color schemes. 100 | 101 | // All the color schemes should be unique 102 | const uniqueSchemes = new Set() 103 | newSchemes.forEach(scheme => uniqueSchemes.add(scheme)) 104 | 105 | const swatches = Array.from(uniqueSchemes) 106 | this.setState({ swatches }) 107 | } 108 | 109 | // Click handler for a palette 110 | updateSwatch = color => { 111 | this.setState({ color }) 112 | 113 | // Invoke the prop callback 114 | this.props.onChange && this.props.onChange(color) 115 | } 116 | 117 | // default onChange handler for color input field 118 | defaultOnChange = color => { 119 | const newColor = new TinyColor(color) 120 | 121 | if (newColor.isValid) { 122 | // Update the color input value 123 | // Also generate the new schemes based on the new color input 124 | this.setState({ color: newColor.toHexString() }, () => 125 | this.generateSchemes(color) 126 | ) 127 | } 128 | } 129 | 130 | copyColor = () => { 131 | clipboard.writeText(this.state.color) 132 | this.setState({ showMsg: true }) 133 | } 134 | 135 | render() { 136 | const { color, swatches, showMsg } = this.state 137 | const { bg, iconColor } = getThemeVariants(this.props.theme) 138 | 139 | return ( 140 | 145 | 146 |
    147 | 151 | 155 | 156 |
    163 | 168 |
    169 |
    170 |
    171 |
    172 | ) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/styles/tooltip.css: -------------------------------------------------------------------------------- 1 | .tooltipped { 2 | position: relative; 3 | } 4 | .tooltipped::after { 5 | position: absolute; 6 | z-index: 1000000; 7 | display: none; 8 | padding: 0.5em 0.75em; 9 | font: normal normal 11px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', 10 | Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 11 | 'Segoe UI Symbol'; 12 | -webkit-font-smoothing: subpixel-antialiased; 13 | color: #fff; 14 | text-align: center; 15 | text-decoration: none; 16 | text-shadow: none; 17 | text-transform: none; 18 | letter-spacing: normal; 19 | word-wrap: break-word; 20 | white-space: pre; 21 | pointer-events: none; 22 | content: attr(aria-label); 23 | background: #1b1f23; 24 | border-radius: 3px; 25 | opacity: 0; 26 | } 27 | .tooltipped::before { 28 | position: absolute; 29 | z-index: 1000001; 30 | display: none; 31 | width: 0; 32 | height: 0; 33 | color: #1b1f23; 34 | pointer-events: none; 35 | content: ''; 36 | border: 6px solid transparent; 37 | opacity: 0; 38 | } 39 | @keyframes tooltip-appear { 40 | from { 41 | opacity: 0; 42 | } 43 | to { 44 | opacity: 1; 45 | } 46 | } 47 | .tooltipped:hover::before, 48 | .tooltipped:hover::after, 49 | .tooltipped:active::before, 50 | .tooltipped:active::after, 51 | .tooltipped:focus::before, 52 | .tooltipped:focus::after { 53 | display: inline-block; 54 | text-decoration: none; 55 | animation-name: tooltip-appear; 56 | animation-duration: 0.1s; 57 | animation-fill-mode: forwards; 58 | animation-timing-function: ease-in; 59 | animation-delay: 0.4s; 60 | } 61 | .tooltipped-no-delay:hover::before, 62 | .tooltipped-no-delay:hover::after, 63 | .tooltipped-no-delay:active::before, 64 | .tooltipped-no-delay:active::after, 65 | .tooltipped-no-delay:focus::before, 66 | .tooltipped-no-delay:focus::after { 67 | animation-delay: 0s; 68 | } 69 | .tooltipped-multiline:hover::after, 70 | .tooltipped-multiline:active::after, 71 | .tooltipped-multiline:focus::after { 72 | display: table-cell; 73 | } 74 | .tooltipped-s::after, 75 | .tooltipped-se::after, 76 | .tooltipped-sw::after { 77 | top: 100%; 78 | right: 50%; 79 | margin-top: 6px; 80 | } 81 | .tooltipped-s::before, 82 | .tooltipped-se::before, 83 | .tooltipped-sw::before { 84 | top: auto; 85 | right: 50%; 86 | bottom: -7px; 87 | margin-right: -6px; 88 | border-bottom-color: #1b1f23; 89 | } 90 | .tooltipped-se::after { 91 | right: auto; 92 | left: 50%; 93 | margin-left: -16px; 94 | } 95 | .tooltipped-sw::after { 96 | margin-right: -16px; 97 | } 98 | .tooltipped-n::after, 99 | .tooltipped-ne::after, 100 | .tooltipped-nw::after { 101 | right: 50%; 102 | bottom: 100%; 103 | margin-bottom: 6px; 104 | } 105 | .tooltipped-n::before, 106 | .tooltipped-ne::before, 107 | .tooltipped-nw::before { 108 | top: -7px; 109 | right: 50%; 110 | bottom: auto; 111 | margin-right: -6px; 112 | border-top-color: #1b1f23; 113 | } 114 | .tooltipped-ne::after { 115 | right: auto; 116 | left: 50%; 117 | margin-left: -16px; 118 | } 119 | .tooltipped-nw::after { 120 | margin-right: -16px; 121 | } 122 | .tooltipped-s::after, 123 | .tooltipped-n::after { 124 | transform: translateX(50%); 125 | } 126 | .tooltipped-w::after { 127 | right: 100%; 128 | bottom: 50%; 129 | margin-right: 6px; 130 | transform: translateY(50%); 131 | } 132 | .tooltipped-w::before { 133 | top: 50%; 134 | bottom: 50%; 135 | left: -7px; 136 | margin-top: -6px; 137 | border-left-color: #1b1f23; 138 | } 139 | .tooltipped-e::after { 140 | bottom: 50%; 141 | left: 100%; 142 | margin-left: 6px; 143 | transform: translateY(50%); 144 | } 145 | .tooltipped-e::before { 146 | top: 50%; 147 | right: -7px; 148 | bottom: 50%; 149 | margin-top: -6px; 150 | border-right-color: #1b1f23; 151 | } 152 | .tooltipped-align-right-1::after, 153 | .tooltipped-align-right-2::after { 154 | right: 0; 155 | margin-right: 0; 156 | } 157 | .tooltipped-align-right-1::before { 158 | right: 10px; 159 | } 160 | .tooltipped-align-right-2::before { 161 | right: 15px; 162 | } 163 | .tooltipped-align-left-1::after, 164 | .tooltipped-align-left-2::after { 165 | left: 0; 166 | margin-left: 0; 167 | } 168 | .tooltipped-align-left-1::before { 169 | left: 5px; 170 | } 171 | .tooltipped-align-left-2::before { 172 | left: 10px; 173 | } 174 | .tooltipped-multiline::after { 175 | width: -webkit-max-content; 176 | width: -moz-max-content; 177 | width: max-content; 178 | max-width: 250px; 179 | word-wrap: break-word; 180 | white-space: pre-line; 181 | border-collapse: separate; 182 | } 183 | .tooltipped-multiline.tooltipped-s::after, 184 | .tooltipped-multiline.tooltipped-n::after { 185 | right: auto; 186 | left: 50%; 187 | transform: translateX(-50%); 188 | } 189 | .tooltipped-multiline.tooltipped-w::after, 190 | .tooltipped-multiline.tooltipped-e::after { 191 | right: 100%; 192 | } 193 | @media screen and (min-width: 0\0) { 194 | .tooltipped-multiline::after { 195 | width: 250px; 196 | } 197 | } 198 | .tooltipped-sticky::before, 199 | .tooltipped-sticky::after { 200 | display: inline-block; 201 | } 202 | .tooltipped-sticky.tooltipped-multiline::after { 203 | display: table-cell; 204 | } 205 | -------------------------------------------------------------------------------- /src/utils/colors.js: -------------------------------------------------------------------------------- 1 | import { TinyColor } from '@ctrl/tinycolor' 2 | import generateColors from 'randomcolor' 3 | 4 | // Copied from react-color/helpers/colors 5 | 6 | function toState(data, oldHue) { 7 | const color = data.hex ? new TinyColor(data.hex) : new TinyColor(data) 8 | const hsl = color.toHsl() 9 | const hsv = color.toHsv() 10 | const rgb = color.toRgb() 11 | const hex = color.toHex() 12 | if (hsl.s === 0) { 13 | hsl.h = oldHue || 0 14 | hsv.h = oldHue || 0 15 | } 16 | const transparent = hex === '000000' && rgb.a === 0 17 | 18 | return { 19 | hsl, 20 | hex: transparent ? 'transparent' : `#${hex}`, 21 | rgb, 22 | hsv, 23 | oldHue: data.h || oldHue || hsl.h, 24 | source: data.source 25 | } 26 | } 27 | 28 | // Copied from react-color/helpers/colors 29 | 30 | /* eslint-disable import/prefer-default-export */ 31 | export function getContrastingColor(data) { 32 | if (!data) { 33 | return '#fff' 34 | } 35 | const col = toState(data) 36 | if (col.hex === 'transparent') { 37 | return 'rgba(0,0,0,0.4)' 38 | } 39 | const yiq = (col.rgb.r * 299 + col.rgb.g * 587 + col.rgb.b * 114) / 1000 40 | return yiq >= 128 ? '#000' : '#fff' 41 | } 42 | 43 | // Returns a set of random colors (this is used in generating different gradients) 44 | export const randomColors = () => { 45 | let i = 0 46 | const newColors = new Set() 47 | 48 | while (i < 3) { 49 | newColors.add(generateColors()) 50 | i += 1 51 | } 52 | 53 | return newColors 54 | } 55 | 56 | // Color conversion helpers 57 | export const utils = { 58 | toRGB: color => new TinyColor(color).toRgbString(), 59 | toHSL: color => new TinyColor(color).toHslString(), 60 | toHSV: color => new TinyColor(color).toHsvString(), 61 | toRGBPercent: color => new TinyColor(color).toPercentageRgbString() 62 | } 63 | -------------------------------------------------------------------------------- /src/utils/constants.js: -------------------------------------------------------------------------------- 1 | // DARK THEME 2 | export const DARK_COLOR = '#1f1f1f' 3 | // LIGHT THEME 4 | export const LIGHT_COLOR = 'rgb(255, 255, 255)' 5 | // DEFAULT GRADIENT COLOR ONE 6 | export const DEFAULT_COLOR_ONE = '#81FFEF' 7 | // DEFAULT GRADIENT COLOR TWO 8 | export const DEFAULT_COLOR_TWO = '#F067B4' 9 | // GRADIENT CONTAINER WIDTH 10 | export const GRADIENT_CONTAINER_WIDTH = '170px' 11 | // GRADIENT CONTAINER HEIGHT 12 | export const GRADIENT_CONTAINER_HEIGHT = '295px' 13 | export const DEFAULT_SWATCHES = [ 14 | '#5a80b4', 15 | '#40e0d0', 16 | '#088da5', 17 | '#f6546a', 18 | '#cac8a0', 19 | '#0079cf', 20 | '#ffa6ca', 21 | '#03ec13', 22 | '#3999dc', 23 | '#e1c9ec', 24 | '#2f9d66', 25 | '#daa520' 26 | ] 27 | // DEFAULT COLOR INPUT 28 | export const DEFAULT_COLOR = '#088da5' 29 | // REQUIRED FOR GENERATING SWATCHES FROM AN IMAGE 30 | export const MAX_COLORS = 64 31 | // WIDTH OF THE COLOR PICKER 32 | export const COLOR_CONTAINER_WIDTH = '228px' 33 | // HEIGHT OF THE COLOR PICKER 34 | export const COLOR_CONTAINER_HEIGHT = '295px' 35 | // DEFAULT COLOR STOP POSITION 36 | export const DEFAULT_COLOR_STOP = 0.2 37 | // WIDTH OF SCHEME PICKER 38 | export const SCHEME_CONTAINER_WIDTH = '200px' 39 | // HEIGHT OF SCHEME PICKER 40 | export const SCHEME_CONTAINER_HEIGHT = '225px' 41 | -------------------------------------------------------------------------------- /src/utils/context.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react' 2 | 3 | export const { Provider, Consumer } = createContext() 4 | -------------------------------------------------------------------------------- /src/utils/theme.js: -------------------------------------------------------------------------------- 1 | import { DARK_COLOR, LIGHT_COLOR } from './constants' 2 | 3 | const getThemeVariants = theme => ({ 4 | bg: theme === 'dark' ? DARK_COLOR : LIGHT_COLOR, 5 | iconColor: theme === 'dark' ? LIGHT_COLOR : DARK_COLOR 6 | }) 7 | 8 | export default getThemeVariants 9 | -------------------------------------------------------------------------------- /stories/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { css } from 'emotion' 4 | 5 | import { BasicPicker, GradientPicker, SchemePicker } from '../src' 6 | 7 | const styles = { 8 | display: 'flex', 9 | justifyContent: 'center', 10 | alignItems: 'center', 11 | marginTop: 20 12 | } 13 | 14 | const Container = props =>
    {props.children}
    15 | 16 | class TestBasicPicker extends React.Component { 17 | state = { 18 | color: 'hotpink' 19 | } 20 | 21 | render() { 22 | const { color } = this.state 23 | 24 | return ( 25 |
    26 | this.setState({ color: c })} 29 | showTools 30 | theme="dark" 31 | /> 32 |

    Basic Color Picker

    33 |
    34 | ) 35 | } 36 | } 37 | 38 | class TestGradientPicker extends React.Component { 39 | state = { 40 | gradient: '' 41 | } 42 | 43 | render() { 44 | const { gradient } = this.state 45 | 46 | return ( 47 |
    48 | this.setState({ gradient: grad })} 53 | /> 54 |

    61 | React Gradient Tools 62 |

    63 |
    64 | ) 65 | } 66 | } 67 | 68 | class TestSchemePicker extends React.Component { 69 | state = { color: 'hotpink' } 70 | 71 | render() { 72 | return ( 73 |
    81 |

    React Color Tools

    82 | this.setState({ color })} 86 | /> 87 |
    88 | ) 89 | } 90 | } 91 | 92 | storiesOf('Basic Color Picker', module) 93 | .add('with basic tools', () => ( 94 | 95 | 96 | 97 | )) 98 | .add('with advance tools', () => ( 99 | 100 | 101 | 102 | )) 103 | .add('with dark theme', () => ( 104 | 105 | 106 | 107 | )) 108 | .add('with parent component and keeping the state in sync', () => ( 109 | 110 | 111 | 112 | )) 113 | 114 | storiesOf('Gradient Picker', module) 115 | .add('with default props', () => ( 116 | 117 | 118 | 119 | )) 120 | .add('with dark theme', () => ( 121 | 122 | 123 | 124 | )) 125 | .add('with parent component and keeping the state in sync', () => ( 126 | 127 | 128 | 129 | )) 130 | .add('with gradient mode and direction', () => ( 131 | 132 | 133 | 134 | )) 135 | .add('with reverse mode', () => ( 136 | 137 | 138 | 139 | )) 140 | 141 | storiesOf('Scheme Picker', module) 142 | .add('with default props', () => ( 143 | 144 | 145 | 146 | )) 147 | .add('with different color scheme', () => ( 148 | 149 | 150 | 151 | )) 152 | .add('with dark theme', () => ( 153 | 154 | 155 | 156 | )) 157 | .add('with parent component and keeping the state in sync', () => ( 158 | 159 | 160 | 161 | )) 162 | -------------------------------------------------------------------------------- /test-setup.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme' 2 | import Adapter from 'enzyme-adapter-react-16' 3 | 4 | configure({ adapter: new Adapter() }) 5 | -------------------------------------------------------------------------------- /tests/Basic-Picker.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import renderer from 'react-test-renderer' 3 | import { mount, shallow } from 'enzyme' 4 | 5 | import { BasicPicker } from '../src' 6 | 7 | const DEFAULT_SWATCHES = [ 8 | '#5a80b4', 9 | '#40e0d0', 10 | '#088da5', 11 | '#f6546a', 12 | '#cac8a0', 13 | '#0079cf', 14 | '#ffa6ca', 15 | '#03ec13', 16 | '#3999dc', 17 | '#e1c9ec', 18 | '#2f9d66', 19 | '#daa520' 20 | ] 21 | const PINK_TINTS = [ 22 | '#ff78bc', 23 | '#ff87c3', 24 | '#ff96cb', 25 | '#ffa5d2', 26 | '#ffb4da', 27 | '#ffc3e1', 28 | '#ffd2e9', 29 | '#ffe1f0', 30 | '#fff0f8', 31 | '#ffffff' 32 | ] 33 | const PINK_SHADES = [ 34 | '#e65fa2', 35 | '#cc5490', 36 | '#b34a7e', 37 | '#993f6c', 38 | '#80355a', 39 | '#662a48', 40 | '#4d2036', 41 | '#331524', 42 | '#190a12', 43 | '#000000' 44 | ] 45 | 46 | class App extends React.Component { 47 | state = { color: 'hotpink' } 48 | 49 | render() { 50 | return ( 51 | 52 |

    React Color Tools

    53 | this.setState({ color })} 57 | showTools={this.props.showTools} 58 | triangle={this.props.triangle} 59 | maxColors={this.props.maxColors} 60 | onSwatchHover={color => this.setState({ color })} 61 | swatches={this.props.swatches} 62 | /> 63 |
    64 | ) 65 | } 66 | } 67 | 68 | describe('Test BasicPicker API', () => { 69 | it('should render the basic picker', () => { 70 | const tree = renderer.create().toJSON() 71 | expect(tree).toMatchSnapshot() 72 | }) 73 | 74 | it('should render the basic picker with dark theme', () => { 75 | const tree = renderer.create().toJSON() 76 | expect(tree).toMatchSnapshot() 77 | }) 78 | 79 | it('should render the advance tools', () => { 80 | const tree = renderer.create().toJSON() 81 | expect(tree).toMatchSnapshot() 82 | }) 83 | 84 | it('should render the picker with user defined swatches', () => { 85 | const tree = renderer 86 | .create() 87 | .toJSON() 88 | expect(tree).toMatchSnapshot() 89 | }) 90 | 91 | it('should render the picker without triangle', () => { 92 | const tree = renderer.create().toJSON() 93 | expect(tree).toMatchSnapshot() 94 | }) 95 | 96 | it('should update the color state when a swatch is clicked', () => { 97 | const Wrapper = mount() 98 | 99 | Wrapper.find('Swatch').forEach((node, i) => { 100 | if (i === 3) { 101 | node.simulate('click') 102 | } 103 | }) 104 | 105 | expect(Wrapper.state('color')).toEqual('#f6546a') 106 | }) 107 | 108 | it('should update the color state on hovering over a swatch', () => { 109 | const Wrapper = mount() 110 | 111 | Wrapper.find('Swatch').forEach((node, i) => { 112 | if (i === 8) { 113 | node.simulate('mouseover') 114 | } 115 | }) 116 | 117 | expect(Wrapper.state('color')).toEqual('#3999dc') 118 | }) 119 | 120 | it('should update the color when color input changes', () => { 121 | const Wrapper = mount() 122 | 123 | Wrapper.find('ColorInputField').simulate('change', { 124 | target: { value: 'mistyrose' } 125 | }) 126 | 127 | expect(Wrapper.state('color')).toEqual('mistyrose') 128 | }) 129 | 130 | it('should update the swatches when new swatches are generated', () => { 131 | const Wrapper = mount() 132 | 133 | Wrapper.find('SwatchesGenerator').simulate('click') 134 | 135 | // Swatches are generated randomly, so we cannot assume it to be equal to a constant value 136 | expect(Wrapper.state('swatches')).not.toEqual(DEFAULT_SWATCHES) 137 | }) 138 | 139 | it('should generate tints of a color', () => { 140 | const Wrapper = mount() 141 | 142 | Wrapper.find('TintsGenerator').simulate('click') 143 | 144 | expect(Wrapper.state('tints')).toEqual(PINK_TINTS) 145 | }) 146 | 147 | it('should generate shades of a color', () => { 148 | const Wrapper = mount() 149 | 150 | Wrapper.find('ShadesGenerator').simulate('click') 151 | 152 | expect(Wrapper.state('shades')).toEqual(PINK_SHADES) 153 | }) 154 | 155 | it('should reset the picker state', () => { 156 | const Wrapper = mount() 157 | 158 | Wrapper.setState({ tints: PINK_TINTS }) 159 | Wrapper.setState({ shades: PINK_SHADES }) 160 | 161 | Wrapper.find('Reset').simulate('click') 162 | 163 | expect(Wrapper.state('tints')).toEqual([]) 164 | expect(Wrapper.state('shades')).toEqual([]) 165 | }) 166 | 167 | it('should apply spin operation to a color', () => { 168 | const Wrapper = mount() 169 | 170 | Wrapper.find('ColorSpinner') 171 | .find('Slider') 172 | .simulate('change', { target: { value: '120' } }) 173 | 174 | expect(Wrapper.state('color')).toEqual('#b4ff69') 175 | }) 176 | 177 | it('should apply saturation operation to a color', () => { 178 | const Wrapper = mount() 179 | 180 | Wrapper.find('ColorSaturator') 181 | .find('Slider') 182 | .simulate('change', { target: { value: '2' } }) 183 | 184 | expect(Wrapper.state('color')).toEqual('#ff69b4') 185 | }) 186 | 187 | it('should apply desaturation operation to a color', () => { 188 | const Wrapper = mount() 189 | 190 | Wrapper.find('ColorDesaturator') 191 | .find('Slider') 192 | .simulate('change', { target: { value: '80' } }) 193 | 194 | expect(Wrapper.state('color')).toEqual('#c3a5b4') 195 | }) 196 | 197 | it('should apply dark operation to a color', () => { 198 | const Wrapper = mount() 199 | 200 | Wrapper.find('ColorDarkener') 201 | .find('Slider') 202 | .simulate('change', { target: { value: '40' } }) 203 | 204 | expect(Wrapper.state('color')).toEqual('#9c004e') 205 | }) 206 | 207 | it('should apply bright operation to a color', () => { 208 | const Wrapper = mount() 209 | 210 | Wrapper.find('ColorBrightener') 211 | .find('Slider') 212 | .simulate('change', { target: { value: '10' } }) 213 | 214 | expect(Wrapper.state('color')).toEqual('#ff82cd') 215 | }) 216 | }) 217 | -------------------------------------------------------------------------------- /tests/Gradient-Picker.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import renderer from 'react-test-renderer' 3 | import { mount, shallow } from 'enzyme' 4 | 5 | import { GradientPicker } from '../src' 6 | 7 | class App extends React.Component { 8 | state = { gradient: '' } 9 | 10 | render() { 11 | const { gradient: grad } = this.state 12 | 13 | return ( 14 | 15 | this.setState({ gradient })} 19 | /> 20 |

    27 | React Gradient Picker 28 |

    29 |
    30 | ) 31 | } 32 | } 33 | 34 | describe('Test GradientPicker API', () => { 35 | it('should render the gradient picker', () => { 36 | const tree = renderer.create().toJSON() 37 | expect(tree).toMatchSnapshot() 38 | }) 39 | 40 | it('should render the gradient picker with dark theme', () => { 41 | const tree = renderer.create().toJSON() 42 | expect(tree).toMatchSnapshot() 43 | }) 44 | 45 | it('should update the state of parent component when onChange is invoked on first render', () => { 46 | const Wrapper = mount() 47 | 48 | expect(Wrapper.state('gradient')).toEqual( 49 | 'linear-gradient(to right, rgb(129, 255, 239) 0%, rgb(240, 103, 180) 100%)' 50 | ) 51 | }) 52 | 53 | it('should create gradient with mode and direction prop', () => { 54 | const Wrapper = mount() 55 | expect(Wrapper.state('gradient')).toEqual( 56 | 'linear-gradient(to left, rgb(129, 255, 239) 0%, rgb(240, 103, 180) 100%)' 57 | ) 58 | }) 59 | 60 | it('should create new gradient based when color input changes', () => { 61 | const Wrapper = mount() 62 | 63 | Wrapper.find('ColorInputField').forEach((node, i) => { 64 | if (i === 1) { 65 | // Color input one 66 | node.simulate('change', { target: { value: 'red' } }) 67 | } else { 68 | // Color input two 69 | node.simulate('change', { target: { value: 'orange' } }) 70 | } 71 | }) 72 | 73 | expect(Wrapper.state('gradient')).toEqual( 74 | 'linear-gradient(to right, rgb(255, 165, 0) 0%, rgb(255, 0, 0) 100%)' 75 | ) 76 | }) 77 | 78 | it('should update the gradient when color stop position changes', () => { 79 | const Wrapper = mount() 80 | 81 | Wrapper.find('Slider').forEach((node, i) => { 82 | if (i === 1) { 83 | // Color stop one 84 | node.simulate('change', { target: { value: '8' } }) 85 | } else { 86 | // Color stop two 87 | node.simulate('change', { target: { value: '4' } }) 88 | } 89 | }) 90 | 91 | expect(Wrapper.state('gradient')).toEqual( 92 | 'linear-gradient(to right, rgb(129, 255, 239) 0%, rgb(129, 255, 239) 20%, rgb(240, 103, 180) 80%, rgb(240, 103, 180) 100%)' 93 | ) 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /tests/Scheme-Picker.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import renderer from 'react-test-renderer' 3 | import { mount, shallow } from 'enzyme' 4 | 5 | import { SchemePicker } from '../src' 6 | 7 | class App extends React.Component { 8 | state = { color: 'hotpink' } 9 | 10 | render() { 11 | return ( 12 | 13 |

    React Color Tools

    14 | this.setState({ color })} 19 | /> 20 |
    21 | ) 22 | } 23 | } 24 | 25 | describe('Test SchemePicker API', () => { 26 | it('should render the scheme picker', () => { 27 | const tree = renderer.create().toJSON() 28 | expect(tree).toMatchSnapshot() 29 | }) 30 | 31 | it('should render the picker with dark theme', () => { 32 | const tree = renderer.create().toJSON() 33 | expect(tree).toMatchSnapshot() 34 | }) 35 | 36 | it('should update the state of parent component when a swatch is clicked', () => { 37 | const Wrapper = mount() 38 | expect(Wrapper.state('color')).toEqual('hotpink') 39 | 40 | Wrapper.find('Swatch').forEach((node, i) => { 41 | // Select a random swatch and fire click event 42 | if (i === 4) { 43 | node.simulate('click') 44 | } 45 | }) 46 | 47 | expect(Wrapper.state('color')).toEqual('#d45896') 48 | }) 49 | 50 | it('should update the color state when passed a new color scheme', () => { 51 | const Wrapper = mount() 52 | const instance = Wrapper.instance() 53 | 54 | expect(Wrapper.state('color')).toEqual('hotpink') 55 | 56 | Wrapper.find('Swatch').forEach((node, i) => { 57 | // Select a random swatch and fire click event 58 | if (i === 7) { 59 | node.simulate('click') 60 | } 61 | }) 62 | 63 | expect(Wrapper.state('color')).toEqual('#fff069') 64 | }) 65 | }) 66 | -------------------------------------------------------------------------------- /tests/__snapshots__/Basic-Picker.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Test BasicPicker API should render the advance tools 1`] = ` 4 |
    7 |
    10 |
    18 |
    26 | #088da5 27 |
    28 |
    29 |
    36 |
    39 |
    49 |
    59 |
    69 |
    79 |
    89 |
    99 |
    109 |
    119 |
    129 |
    139 |
    149 |
    159 |
    160 | 169 |
    172 | 198 |
    199 |
    202 | 206 | 217 | 224 | 225 | image picker 226 | 227 | 231 | 232 | 233 | 240 | 246 | 247 | swatches generator 248 | 249 | 253 | 254 | 255 | 262 | 268 | 269 | tints generator 270 | 271 | 275 | 276 | 277 | 284 | 290 | 291 | shades generator 292 | 293 | 297 | 298 | 299 | 307 | 313 | 314 | clipboard 315 | 316 | 320 | 321 | 322 | 329 | 335 | 336 | reset state 337 | 338 | 342 | 343 | 344 |
    345 |
    346 |
      349 |
    • 350 | 354 | 360 | 361 | color spin 362 | 363 | 367 | 368 | 376 | 385 | 386 |
    • 387 |
    • 388 | 392 | 398 | 399 | color saturator 400 | 401 | 405 | 406 | 414 | 420 | 421 |
    • 422 |
    • 423 | 427 | 433 | 434 | color desaturator 435 | 436 | 440 | 441 | 449 | 455 | 456 |
    • 457 |
    • 458 | 462 | 468 | 469 | color darkener 470 | 471 | 475 | 476 | 484 | 490 | 491 |
    • 492 |
    • 493 | 497 | 503 | 504 | color brightener 505 | 506 | 510 | 511 | 519 | 525 | 526 |
    • 527 |
    528 |
    529 |
    530 |
    531 | `; 532 | 533 | exports[`Test BasicPicker API should render the basic picker 1`] = ` 534 |
    537 |
    540 |
    548 |
    556 | #088da5 557 |
    558 |
    559 |
    566 |
    569 |
    579 |
    589 |
    599 |
    609 |
    619 |
    629 |
    639 |
    649 |
    659 |
    669 |
    679 |
    689 |
    690 | 699 |
    702 | 728 |
    729 |
    732 | 736 | 747 | 754 | 755 | image picker 756 | 757 | 761 | 762 | 763 | 770 | 776 | 777 | swatches generator 778 | 779 | 783 | 784 | 785 | 792 | 798 | 799 | tints generator 800 | 801 | 805 | 806 | 807 | 814 | 820 | 821 | shades generator 822 | 823 | 827 | 828 | 829 | 837 | 843 | 844 | clipboard 845 | 846 | 850 | 851 | 852 | 859 | 865 | 866 | reset state 867 | 868 | 872 | 873 | 874 |
    875 |
    876 |
    877 | `; 878 | 879 | exports[`Test BasicPicker API should render the basic picker with dark theme 1`] = ` 880 |
    883 |
    886 |
    894 |
    902 | #088da5 903 |
    904 |
    905 |
    912 |
    915 |
    925 |
    935 |
    945 |
    955 |
    965 |
    975 |
    985 |
    995 |
    1005 |
    1015 |
    1025 |
    1035 |
    1036 | 1045 |
    1048 | 1074 |
    1075 |
    1078 | 1082 | 1093 | 1100 | 1101 | image picker 1102 | 1103 | 1107 | 1108 | 1109 | 1116 | 1122 | 1123 | swatches generator 1124 | 1125 | 1129 | 1130 | 1131 | 1138 | 1144 | 1145 | tints generator 1146 | 1147 | 1151 | 1152 | 1153 | 1160 | 1166 | 1167 | shades generator 1168 | 1169 | 1173 | 1174 | 1175 | 1183 | 1189 | 1190 | clipboard 1191 | 1192 | 1196 | 1197 | 1198 | 1205 | 1211 | 1212 | reset state 1213 | 1214 | 1218 | 1219 | 1220 |
    1221 |
    1222 |
    1223 | `; 1224 | 1225 | exports[`Test BasicPicker API should render the picker with user defined swatches 1`] = ` 1226 |
    1229 |
    1232 |
    1240 |
    1248 | #088da5 1249 |
    1250 |
    1251 |
    1258 |
    1261 |
    1271 |
    1281 |
    1282 | 1291 |
    1294 | 1320 |
    1321 |
    1324 | 1328 | 1339 | 1346 | 1347 | image picker 1348 | 1349 | 1353 | 1354 | 1355 | 1362 | 1368 | 1369 | swatches generator 1370 | 1371 | 1375 | 1376 | 1377 | 1384 | 1390 | 1391 | tints generator 1392 | 1393 | 1397 | 1398 | 1399 | 1406 | 1412 | 1413 | shades generator 1414 | 1415 | 1419 | 1420 | 1421 | 1429 | 1435 | 1436 | clipboard 1437 | 1438 | 1442 | 1443 | 1444 | 1451 | 1457 | 1458 | reset state 1459 | 1460 | 1464 | 1465 | 1466 |
    1467 |
    1468 |
    1469 | `; 1470 | 1471 | exports[`Test BasicPicker API should render the picker without triangle 1`] = ` 1472 |
    1475 |
    1483 |
    1491 | #088da5 1492 |
    1493 |
    1494 |
    1501 |
    1504 |
    1514 |
    1524 |
    1534 |
    1544 |
    1554 |
    1564 |
    1574 |
    1584 |
    1594 |
    1604 |
    1614 |
    1624 |
    1625 | 1634 |
    1637 | 1663 |
    1664 |
    1667 | 1671 | 1682 | 1689 | 1690 | image picker 1691 | 1692 | 1696 | 1697 | 1698 | 1705 | 1711 | 1712 | swatches generator 1713 | 1714 | 1718 | 1719 | 1720 | 1727 | 1733 | 1734 | tints generator 1735 | 1736 | 1740 | 1741 | 1742 | 1749 | 1755 | 1756 | shades generator 1757 | 1758 | 1762 | 1763 | 1764 | 1772 | 1778 | 1779 | clipboard 1780 | 1781 | 1785 | 1786 | 1787 | 1794 | 1800 | 1801 | reset state 1802 | 1803 | 1807 | 1808 | 1809 |
    1810 |
    1811 |
    1812 | `; 1813 | -------------------------------------------------------------------------------- /tests/__snapshots__/Gradient-Picker.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Test GradientPicker API should render the gradient picker 1`] = ` 4 |
    7 |
    15 |
    22 |
    23 | 32 | 33 | 39 | 54 | 55 |
    56 |
    57 | 66 | 67 | 73 | 88 | 89 |
    90 |
    93 | 101 | 107 | 108 | clipboard 109 | 110 | 114 | 115 | 116 |
    123 | 130 | 136 | 137 | gradient generator 138 | 139 | 143 | 144 | 145 |
    146 |
    147 |
    148 |
    149 | `; 150 | 151 | exports[`Test GradientPicker API should render the gradient picker with dark theme 1`] = ` 152 |
    155 |
    163 |
    170 |
    171 | 180 | 181 | 187 | 202 | 203 |
    204 |
    205 | 214 | 215 | 221 | 236 | 237 |
    238 |
    241 | 249 | 255 | 256 | clipboard 257 | 258 | 262 | 263 | 264 |
    271 | 278 | 284 | 285 | gradient generator 286 | 287 | 291 | 292 | 293 |
    294 |
    295 |
    296 |
    297 | `; 298 | -------------------------------------------------------------------------------- /tests/__snapshots__/Scheme-Picker.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Test SchemePicker API should render the picker with dark theme 1`] = ` 4 |
    7 |
    15 |
    23 | #ff69b4 24 |
    25 |
    26 |
    33 | 42 |
    45 |
    50 |
    55 |
    60 |
    65 |
    70 |
    75 |
    80 |
    85 |
    90 |
    95 |
    100 |
    105 |
    110 |
    115 |
    120 |
    125 |
    130 |
    135 |
    140 |
    145 |
    150 |
    155 |
    160 |
    165 |
    170 |
    175 |
    180 |
    185 |
    190 |
    195 |
    196 |
    199 | 207 | 213 | 214 | clipboard 215 | 216 | 220 | 221 | 222 |
    223 |
    224 |
    225 | `; 226 | 227 | exports[`Test SchemePicker API should render the scheme picker 1`] = ` 228 |
    231 |
    239 |
    247 | #ff69b4 248 |
    249 |
    250 |
    257 | 266 |
    269 |
    274 |
    279 |
    284 |
    289 |
    294 |
    299 |
    304 |
    309 |
    314 |
    319 |
    324 |
    329 |
    334 |
    339 |
    344 |
    349 |
    354 |
    359 |
    364 |
    369 |
    374 |
    379 |
    384 |
    389 |
    394 |
    399 |
    404 |
    409 |
    414 |
    419 |
    420 |
    423 | 431 | 437 | 438 | clipboard 439 | 440 | 444 | 445 | 446 |
    447 |
    448 |
    449 | `; 450 | -------------------------------------------------------------------------------- /tests/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "react-scripts": "^2.0.3" 8 | }, 9 | "dependencies": { 10 | "coolhue": "^1.0.9", 11 | "emotion": "^9.2.12", 12 | "react": "^16.5.2", 13 | "react-color-tools": "^1.0.0", 14 | "react-dom": "^16.5.2" 15 | }, 16 | "scripts": { 17 | "start": "SKIP_PREFLIGHT_CHECK=true react-scripts start", 18 | "build": "SKIP_PREFLIGHT_CHECK=true react-scripts build", 19 | "deploy": "yarn build && surge" 20 | }, 21 | "browserslist": [ 22 | ">0.2%", 23 | "not dead", 24 | "not ie <= 11", 25 | "not op_mini all" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /website/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Color Tools 6 | 7 | 8 | 9 |
    10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /website/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import { css, injectGlobal } from 'emotion' 4 | import { BasicPicker, SchemePicker, GradientPicker } from 'react-color-tools' 5 | import coolhue from 'coolhue' 6 | 7 | const BASIC_PICKER = 'Basic Color Picker' 8 | const SCHEME_PICKER = 'Color Scheme Picker' 9 | const GRADIENT_PICKER = 'Gradient Picker' 10 | 11 | injectGlobal` 12 | body { 13 | font-family: 'Source Sans Pro', sans-serif; 14 | color: #2f2f2f; 15 | } 16 | ` 17 | 18 | const shadow = css` 19 | -webkit-box-shadow: 10px 10px 9px -11px rgba(122, 121, 122, 1); 20 | -moz-box-shadow: 10px 10px 9px -11px rgba(122, 121, 122, 1); 21 | box-shadow: 10px 10px 9px -11px rgba(122, 121, 122, 1); 22 | ` 23 | 24 | const Heading = props => ( 25 |

    40 | {props.children} 41 |

    42 | ) 43 | 44 | Heading.defaultProps = { 45 | gradientPicker: false 46 | } 47 | 48 | const Description = props => ( 49 |

    60 | A set of tools as React components for working with{' '} 61 | 71 | colors 72 | 73 |

    74 | ) 75 | 76 | const GitHubLink = props => ( 77 | 88 | ) 89 | 90 | const Container = props => ( 91 |
    100 | {props.children} 101 |
    102 | ) 103 | 104 | const Link = ({ underline, url, children, ...rest }) => ( 105 | 120 | {children} 121 | 122 | ) 123 | 124 | const Footer = props => ( 125 |
    126 |

    132 | Made with{' '} 133 | 134 | ❤️ 135 | {' '} 136 | by{' '} 137 | 138 | Nitin Tulswani 139 | 140 |

    141 |
    142 | ) 143 | 144 | class App extends React.Component { 145 | state = { 146 | color: 'pink', 147 | pickers: ['Basic Color Picker', 'Gradient Picker', 'Color Scheme Picker'], 148 | currentPicker: 'Basic Color Picker', 149 | gradient: '', 150 | descGrad: coolhue.getGradientStyle(5) 151 | } 152 | 153 | changeFormat = e => this.setState({ currentPicker: e.target.value }) 154 | 155 | renderPickerOptions = () => { 156 | const { pickers } = this.state 157 | 158 | return pickers.map(picker => ( 159 | 162 | )) 163 | } 164 | 165 | getRandomValue = (min, max) => Math.random() * (max - min) + min 166 | 167 | updateGradient = e => { 168 | const number = Math.floor(this.getRandomValue(1, 60)) 169 | 170 | this.setState({ descGrad: coolhue.getGradientStyle(number) }) 171 | } 172 | 173 | render() { 174 | return ( 175 |
    176 | 177 | 185 | React Color Tools 186 | 187 | 191 |
    192 | {this.state.currentPicker === BASIC_PICKER && ( 193 | this.setState({ color })} 197 | showTools 198 | /> 199 | )} 200 | {this.state.currentPicker === SCHEME_PICKER && ( 201 | this.setState({ color })} 205 | /> 206 | )} 207 | {this.state.currentPicker === GRADIENT_PICKER && ( 208 | this.setState({ gradient })} 210 | /> 211 | )} 212 |
    213 |
    214 | 217 |
    218 | 219 |
    220 | 221 | Read the detailed documentation 222 | 223 |
    224 |
    225 | 226 |
    227 | ) 228 | } 229 | } 230 | 231 | render(, document.getElementById('root')) 232 | --------------------------------------------------------------------------------