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 |
177 |
180 | HEX
181 |
182 |
185 | HSV
186 |
187 |
190 | RGB
191 |
192 |
195 | HSL
196 |
197 |
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 |
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 |
707 |
710 | HEX
711 |
712 |
715 | HSV
716 |
717 |
720 | RGB
721 |
722 |
725 | HSL
726 |
727 |
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 |
1053 |
1056 | HEX
1057 |
1058 |
1061 | HSV
1062 |
1063 |
1066 | RGB
1067 |
1068 |
1071 | HSL
1072 |
1073 |
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 |
1282 |
1291 |
1294 |
1299 |
1302 | HEX
1303 |
1304 |
1307 | HSV
1308 |
1309 |
1312 | RGB
1313 |
1314 |
1317 | HSL
1318 |
1319 |
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 |
1642 |
1645 | HEX
1646 |
1647 |
1650 | HSV
1651 |
1652 |
1655 | RGB
1656 |
1657 |
1660 | HSL
1661 |
1662 |
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 |
149 | `;
150 |
151 | exports[`Test GradientPicker API should render the gradient picker with dark theme 1`] = `
152 |
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 |
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 |
160 | {picker}
161 |
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 |
215 | {this.renderPickerOptions()}
216 |
217 |
218 |
219 |
220 |
221 | Read the detailed documentation
222 |
223 |
224 |
225 |
226 |
227 | )
228 | }
229 | }
230 |
231 | render(
, document.getElementById('root'))
232 |
--------------------------------------------------------------------------------