├── .github
└── workflows
│ └── node.js.yml
├── .gitignore
├── .node-version
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── arrow.ai
├── contrast-triangle-logo.ai
├── netlify.toml
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.css
├── App.scss
├── App.test.js
├── App.tsx
├── Constants.tsx
├── arrow-left.svg
├── arrow-right.svg
├── color.d.ts
├── components
│ ├── ColorInput.tsx
│ ├── Footer.tsx
│ ├── PreviewParagraph.tsx
│ ├── ResultCard.tsx
│ ├── ResultEmoji.tsx
│ ├── Results.tsx
│ └── UnderlineControl.tsx
├── contrast-triangle-logo.svg
├── index.css
├── index.tsx
├── logo.svg
├── react-app-env.d.ts
├── serviceWorker.ts
├── setupTests.ts
└── utils
│ ├── calculate-contrast.test.ts
│ ├── calculate-contrast.tsx
│ ├── calculate-overlay.test.ts
│ ├── calculate-overlay.tsx
│ ├── check-yourself-before-you-hex-yourself.test.ts
│ ├── check-yourself-before-you-hex-yourself.tsx
│ ├── color-translate.test.ts
│ ├── color-translate.tsx
│ ├── get-luminance.test.ts
│ ├── get-luminance.tsx
│ ├── named-colors.tsx
│ ├── queryString.tsx
│ ├── to-hex.test.ts
│ ├── to-hex.ts
│ ├── to-rgb.test.ts
│ ├── to-rgb.ts
│ ├── to-rgba.test.ts
│ ├── to-rgba.tsx
│ ├── type-of-color.test.ts
│ ├── type-of-color.tsx
│ └── useQueryString.tsx
├── tsconfig.json
└── yarn.lock
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: CI
5 |
6 | on:
7 | push:
8 | branches: [ main ]
9 | pull_request:
10 | branches: [ main ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [14.x]
20 |
21 | steps:
22 | - name: Checkout repository
23 | uses: actions/checkout@v2
24 |
25 | - name: Use Node.js ${{ matrix.node-version }}
26 | uses: actions/setup-node@v1
27 | with:
28 | node-version: ${{ matrix.node-version }}
29 | - run: yarn --frozen-lockfile
30 | - run: yarn test
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
23 | .env
24 |
--------------------------------------------------------------------------------
/.node-version:
--------------------------------------------------------------------------------
1 | v14.13.1
2 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "workbench.colorCustomizations": {
3 | "titleBar.activeForeground": "#000",
4 | "titleBar.inactiveForeground": "#000000CC",
5 | "titleBar.activeBackground": "#9ACD32",
6 | "titleBar.inactiveBackground": "#6B8E23"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Chip Cullen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | [](https://app.netlify.com/sites/contrast-triangle/deploys)
4 |
5 | # The Contrast Triangle
6 |
7 | https://contrast-triangle.com
8 |
9 | _______________
10 |
11 | ## Prior art
12 |
13 | Inspired by [Lea Verou's](https://lea.verou.me/) excellent [Contrast Ratio](https://contrast-ratio.com/)
14 |
15 | Most of the color translation logic is from this CSS tricks article by [Jon Kanter](https://jonkantner.com/):
16 |
17 | https://css-tricks.com/converting-color-spaces-in-javascript/
18 |
19 | Query Parameter support is from this Medium article by [Fernando Abolafio](https://github.com/fernandoabolafio)
20 |
21 | https://medium.com/swlh/using-react-hooks-to-sync-your-component-state-with-the-url-query-string-81ccdfcb174f
22 |
23 | Luminance calculation function is from this gist by [John Schulz](https://gist.github.com/jfsiii):
24 |
25 | https://gist.github.com/jfsiii/5641126
26 |
27 | On / Off Toggle inspired by the [Toggle Button Inclusive Component Pattern](https://inclusive-components.design/toggle-button/) by [Heydon Pickering](https://heydonworks.com/)
28 |
29 | _______________
30 |
31 | This project was made with [Create React App](https://github.com/facebook/create-react-app).
32 |
33 | ## Available Scripts
34 |
35 | In the project directory, you can run:
36 |
37 | ### `yarn start`
38 |
39 | Runs the app in the development mode.
40 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
41 |
42 | The page will reload if you make edits.
43 | You will also see any lint errors in the console.
44 |
45 | ### `yarn test`
46 | ### `yarn test --watchAll`
47 |
48 | Launches the test runner in the interactive watch mode.
49 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
50 |
51 | ### `yarn build`
52 |
53 | Builds the app for production to the `build` folder.
54 | It correctly bundles React in production mode and optimizes the build for the best performance.
55 |
56 | The build is minified and the filenames include the hashes.
57 | Your app is ready to be deployed!
58 |
59 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
60 |
61 | ### `yarn eject`
62 |
63 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
64 |
65 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
66 |
67 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
68 |
69 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
70 |
71 | ## Learn More
72 |
73 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
74 |
75 | To learn React, check out the [React documentation](https://reactjs.org/).
76 |
--------------------------------------------------------------------------------
/arrow.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chipcullen/contrast-triangle/94d580dac514ccb42aa429686e5bf5089fd44b34/arrow.ai
--------------------------------------------------------------------------------
/contrast-triangle-logo.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chipcullen/contrast-triangle/94d580dac514ccb42aa429686e5bf5089fd44b34/contrast-triangle-logo.ai
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | command = "npm run build"
3 | publish = "build"
4 |
5 | [build.environment]
6 | GENERATE_SOURCEMAP = "true"
7 |
8 | [build.production.environment]
9 | GENERATE_SOURCEMAP = "true"
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "contrast-triangle",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/dom": "^7.26.4",
7 | "@testing-library/jest-dom": "^5.11.5",
8 | "@testing-library/react": "^11.1.1",
9 | "@testing-library/user-event": "^12.1.10",
10 | "@types/jest": "^26.0.15",
11 | "@types/node": "^14.14.6",
12 | "@types/react": "^16.9.55",
13 | "@types/react-dom": "^16.9.9",
14 | "query-string": "^6.13.6",
15 | "react": "^17.0.1",
16 | "react-dom": "^17.0.1",
17 | "react-scripts": "4.0.0",
18 | "sass": "^1.28.0",
19 | "typescript": "~4.0.5"
20 | },
21 | "scripts": {
22 | "start": "react-scripts start",
23 | "build": "react-scripts build",
24 | "test": "react-scripts test",
25 | "eject": "react-scripts eject"
26 | },
27 | "eslintConfig": {
28 | "extends": "react-app"
29 | },
30 | "browserslist": {
31 | "production": [
32 | ">5%",
33 | "not dead",
34 | "not op_mini all"
35 | ],
36 | "development": [
37 | "last 1 chrome version",
38 | "last 1 firefox version",
39 | "last 1 safari version"
40 | ]
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chipcullen/contrast-triangle/94d580dac514ccb42aa429686e5bf5089fd44b34/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | The Contrast Triangle
10 |
11 |
12 |
13 |
14 | You need to enable JavaScript to run this app.
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chipcullen/contrast-triangle/94d580dac514ccb42aa429686e5bf5089fd44b34/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chipcullen/contrast-triangle/94d580dac514ccb42aa429686e5bf5089fd44b34/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Contrast Triange",
3 | "name": "The Contrast Triangle",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; }
3 |
4 | .App {
5 | padding: 3.75vw 0;
6 | height: 100%;
7 | min-height: 100vh; }
8 |
9 | .preview-paragraph {
10 | width: calc(100% - 2rem);
11 | max-width: 37.5rem;
12 | margin: 0 auto 3.75vw auto;
13 | line-height: 1.4; }
14 | .preview-paragraph a {
15 | text-decoration: none; }
16 |
17 | .colorInputs {
18 | width: calc(100% - 2rem);
19 | margin: 0 auto;
20 | display: flex;
21 | flex-direction: column;
22 | justify-content: center; }
23 | @media (min-width: 700px) {
24 | .colorInputs {
25 | flex-direction: row; } }
26 |
27 | .ColorInput {
28 | display: flex;
29 | margin-right: 1rem;
30 | margin-bottom: 0.5rem; }
31 | .ColorInput [type="text"] {
32 | max-width: 10rem;
33 | font-size: 1rem;
34 | padding: 0.1rem 0.25rem;
35 | margin-left: 0.5rem; }
36 |
37 | .UnderlineControl {
38 | text-align: center; }
39 |
40 | .Results {
41 | max-width: 18rem;
42 | margin: 2rem auto;
43 | list-style-type: none;
44 | border: 1px solid black;
45 | padding: 1rem; }
46 | .Results li {
47 | margin-bottom: 0.5rem; }
48 | .Results small {
49 | display: block; }
50 |
--------------------------------------------------------------------------------
/src/App.scss:
--------------------------------------------------------------------------------
1 | $bp: 800px;
2 |
3 | body {
4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
5 | Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
6 | }
7 |
8 | .app {
9 | padding: 3.75vw 0;
10 | height: 100%;
11 | min-height: 100vh;
12 | }
13 |
14 | .app__inner {
15 | max-width: 1000px;
16 | margin: 0 auto;
17 | padding: 20px;
18 | display: grid;
19 | grid-template-columns: 1fr;
20 | grid-template-areas:
21 | "title"
22 | "preview"
23 | "controls";
24 | justify-items: center;
25 |
26 | @media (min-width: $bp) {
27 | grid-template-columns: 1fr 2fr;
28 | grid-template-areas:
29 | "title preview"
30 | "controls controls";
31 | }
32 | }
33 |
34 | header {
35 | grid-area: title;
36 | }
37 |
38 | .logo {
39 | max-width: 400px;
40 |
41 | svg {
42 | width: 100%;
43 | }
44 | }
45 |
46 | .preview-paragraph {
47 | width: calc(100% - 2rem);
48 | max-width: 37.5rem;
49 | margin: 0 auto 3.75vw auto;
50 | line-height: 1.4;
51 | }
52 |
53 | .controls {
54 | grid-area: controls;
55 | display: grid;
56 | grid-template-areas:
57 | "color-input-text"
58 | "color-input-link"
59 | "color-input-bg"
60 | "underline-control";
61 | align-items: center;
62 | justify-content: center;
63 |
64 | @media (min-width: $bp) {
65 | grid-template-columns: 0.5fr 0.5fr 0.5fr 1fr 0.5fr 0.5fr 0.5fr;
66 | grid-template-rows: 1fr 1fr 1fr;
67 |
68 | grid-template-areas:
69 | "color-input-text color-input-text . result-card-link-text . color-input-link color-input-link"
70 | " . result-card-text-bg . underline-control . result-card-link-bg ."
71 | " . . . color-input-bg . . .";
72 | }
73 | }
74 |
75 | .color-input {
76 | display: flex;
77 | margin-right: 1rem;
78 | margin-bottom: 0.5rem;
79 |
80 | label {
81 | font-weight: bold;
82 |
83 | span {
84 | display: block;
85 | }
86 | }
87 |
88 | .color-input__inputs-wrapper {
89 | display: flex;
90 | }
91 |
92 | [type="text"] {
93 | max-width: 10rem;
94 | font-size: 1rem;
95 | padding: 0.1rem 0.25rem;
96 | }
97 | }
98 |
99 | .color-input--text {
100 | grid-area: color-input-text;
101 | }
102 |
103 | .color-input--link {
104 | grid-area: color-input-link;
105 | }
106 |
107 | .color-input--bg {
108 | grid-area: color-input-bg;
109 | }
110 |
111 | .underline-control {
112 | grid-area: underline-control;
113 | text-align: center;
114 | }
115 |
116 | .Results {
117 | max-width: 18rem;
118 | margin: 2rem auto;
119 | list-style-type: none;
120 |
121 | border: 1px solid black;
122 | padding: 1rem;
123 |
124 | li {
125 | margin-bottom: 0.5rem;
126 | }
127 |
128 | small {
129 | display: block;
130 | }
131 |
132 | @media (min-width: $bp) {
133 | display: none;
134 | }
135 | }
136 |
137 | .result-card-wrapper {
138 | position: relative;
139 | }
140 |
141 | .result-card {
142 | display: none;
143 | z-index: 1;
144 | position: relative;
145 |
146 | @media (min-width: $bp) {
147 | display: block;
148 | }
149 |
150 | width: 150px;
151 | padding: 10px 20px 20px 20px;
152 | margin: 0 20px;
153 | clip-path: polygon(0% 0%, 100% 0%, 50% 100%);
154 | background: lightgray;
155 | text-align: center;
156 |
157 | h2 {
158 | margin: 0 0 10px 0;
159 | font-size: 1.2rem;
160 |
161 | span {
162 | display: block;
163 | }
164 | }
165 | }
166 |
167 | @mixin arrow-before($top: 50%, $left: -20px, $rotation: 0) {
168 | &::before {
169 | content: "";
170 | display: block;
171 | width: 110px;
172 | height: 33px;
173 | background-image: url("./arrow-left.svg");
174 | background-repeat: no-repeat;
175 | background-size: cover;
176 | position: absolute;
177 | top: $top;
178 | left: $left;
179 | transform: rotate($rotation);
180 | opacity: 0.5;
181 | }
182 | }
183 |
184 | @mixin arrow-after($top: 50%, $right: 20px, $rotation: 0) {
185 | &::after {
186 | content: "";
187 | display: block;
188 | width: 110px;
189 | height: 33px;
190 | background-image: url("./arrow-right.svg");
191 | background-repeat: no-repeat;
192 | background-size: cover;
193 | position: absolute;
194 | top: $top;
195 | right: $right;
196 | transform: rotate($rotation);
197 | z-index: 0;
198 | opacity: 0.5;
199 | }
200 | }
201 |
202 | .result-card--link-text {
203 | grid-area: result-card-link-text;
204 |
205 | @media (min-width: $bp) {
206 | @include arrow-before();
207 | @include arrow-after();
208 | }
209 | }
210 |
211 | .result-card--bg-text {
212 | grid-area: result-card-text-bg;
213 |
214 | @media (min-width: $bp) {
215 | @include arrow-before(-10px, 50px, 60deg);
216 | @include arrow-after(110px, 0, 60deg);
217 | }
218 | }
219 |
220 | .result-card--bg-link {
221 | grid-area: result-card-link-bg;
222 |
223 | @media (min-width: $bp) {
224 | @include arrow-before(-10px, 50px, 120deg);
225 | @include arrow-after(110px, 130px, 120deg);
226 | }
227 | }
228 |
229 | .ResultEmoji {
230 | cursor: help;
231 | }
232 |
233 | // underline control
234 |
235 | $buttonsize: 32px;
236 |
237 | .underline-control__toggle {
238 | position: relative;
239 | height: $buttonsize;
240 | padding: 4px ($buttonsize / 2);
241 | transition: background-color 0.2s ease-in;
242 | border: 0;
243 | border-radius: ($buttonsize / 2);
244 | box-shadow: inset 0 0 1px 2px hsla(0, 0, 0, 0.2);
245 | color: white;
246 | font-size: 16px;
247 | text-align: center;
248 |
249 | // the circle thingy
250 | &::before {
251 | content: "";
252 | display: block;
253 | position: absolute;
254 | top: 2px;
255 | left: calc(50% - #{$buttonsize / 2});
256 | width: $buttonsize - 4;
257 | height: $buttonsize - 4;
258 | transition: transform 0.2s ease-in;
259 |
260 | @media (prefers-reduced-motion: reduce) {
261 | transition-duration: 0s;
262 | }
263 |
264 | border-radius: 50%;
265 | background-color: lightgray;
266 | filter: drop-shadow(0 1px 2px hsla(0, 0, 0, 0.3));
267 | }
268 |
269 | // the text labels
270 | .enabled,
271 | .disabled {
272 | position: relative;
273 | transition: opacity 0.1s ease-in;
274 | transition-delay: 0.1s;
275 |
276 | @media (prefers-reduced-motion: reduce) {
277 | transition-duration: 0s;
278 | transition-delay: 0s;
279 | }
280 |
281 | pointer-events: none;
282 | }
283 |
284 | .enabled {
285 | left: 0;
286 | }
287 |
288 | .disabled {
289 | left: 0;
290 | }
291 |
292 | &[aria-checked="true"] {
293 | background-color: black;
294 |
295 | &::before {
296 | transform: translateX(24px);
297 | }
298 |
299 | .enabled {
300 | opacity: 1;
301 | }
302 |
303 | .disabled {
304 | opacity: 0;
305 | }
306 | }
307 |
308 | &[aria-checked="false"] {
309 | background-color: #666;
310 |
311 | &::before {
312 | transform: translateX(-20px);
313 | }
314 |
315 | .enabled {
316 | opacity: 0;
317 | }
318 |
319 | .disabled {
320 | opacity: 1;
321 | }
322 | }
323 | }
324 |
325 | footer {
326 | padding: 20px;
327 | text-align: center;
328 | }
329 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 |
5 | it("renders without crashing", () => {
6 | const div = document.createElement("div");
7 | ReactDOM.render( , div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import "./App.scss";
3 |
4 | import ColorInput from "./components/ColorInput";
5 | import PreviewParagraph from "./components/PreviewParagraph";
6 | import Results from "./components/Results";
7 | import ResultCard from "./components/ResultCard";
8 | import UnderlineControl from "./components/UnderlineControl";
9 | import Footer from "./components/Footer";
10 | import { colorTranslate } from "./utils/color-translate";
11 | import { isValidColor } from "./utils/type-of-color";
12 | import { checkYourSelfBeforeYouHexYourself } from "./utils/check-yourself-before-you-hex-yourself";
13 | import { useQueryString } from "./utils/useQueryString";
14 |
15 | import { ReactComponent as Logo } from "./contrast-triangle-logo.svg";
16 |
17 | import {
18 | ASSUMED_BACKGROUND_COLOR,
19 | DEFAULTBGCOLOR,
20 | DEFAULTTEXTCOLOR,
21 | DEFAULTLINKCOLOR,
22 | } from "./Constants";
23 |
24 | const App: React.FC = () => {
25 | const [bgColorQp, setBgColorQp] = useQueryString(`bgColor`);
26 | const [textColorQp, setTextColorQp] = useQueryString(`textColor`);
27 | const [linkColorQp, setLinkColorQp] = useQueryString(`linkColor`);
28 | const [underlinesQp, setUnderlinesQp] = useQueryString(`underlines`, false);
29 |
30 | // We need to set up background color state first
31 | const bgColorInitState: ColorObject = colorTranslate(
32 | // if the query parameter exists, use that, if not use default
33 | bgColorQp ? bgColorQp.toString() : DEFAULTBGCOLOR,
34 | ASSUMED_BACKGROUND_COLOR,
35 | true
36 | )
37 |
38 | const [bgColor, setBgColor] = useState(bgColorInitState);
39 |
40 | // we use this a lot
41 | const bgRgb = bgColor.rgb;
42 |
43 | // Then use background color state when initing the other colors
44 | const textColorInitState: ColorObject = colorTranslate(
45 | // if the query parameter exists, use that, if not use default
46 | textColorQp ? textColorQp.toString() : DEFAULTTEXTCOLOR,
47 | bgRgb,
48 | false
49 | )
50 | const [textColor, setTextColor] = useState(textColorInitState);
51 |
52 | const linkColorInitState: ColorObject = colorTranslate(
53 | // if the query parameter exists, use that, if not use default
54 | linkColorQp ? linkColorQp.toString() : DEFAULTLINKCOLOR,
55 | bgRgb
56 | )
57 | const [linkColor, setLinkColor] = useState(linkColorInitState);
58 |
59 | const textDecorationInitState =
60 | underlinesQp && underlinesQp === `true` ? `underlines` : `none`;
61 |
62 | const [textDecoration, setTextDecoration] = useState(textDecorationInitState);
63 |
64 | const handleTextColorChange = (color: string) => {
65 | if (color !== textColor.userValue && isValidColor(color)) {
66 | setTextColor(colorTranslate(color, bgRgb));
67 | setTextColorQp(color);
68 | }
69 | }
70 |
71 | const handleLinkColorChange = (color: string) => {
72 | if (color !== linkColor.userValue && isValidColor(color)) {
73 | setLinkColor(colorTranslate(color, bgRgb));
74 | setLinkColorQp(color);
75 | }
76 | }
77 |
78 | const handleBgColorChange = (color: string) => {
79 | if (color !== bgColor.userValue && isValidColor(color)) {
80 | // first set the background color
81 | setBgColor(colorTranslate(color, bgRgb, true));
82 | setBgColorQp(color);
83 |
84 | // then re-translate the text and link colors
85 | // if they have alpha values
86 | if (textColor.alpha) {
87 | setTextColor(colorTranslate(textColor.userValue, bgRgb));
88 | }
89 |
90 | if (linkColor.alpha) {
91 | setLinkColor(colorTranslate(linkColor.userValue, bgRgb));
92 | }
93 | }
94 | }
95 |
96 | const handleUnderlineChange = (checked: boolean) => {
97 | const underlineState = checked ? `underline` : `none`;
98 | setTextDecoration(underlineState);
99 | setUnderlinesQp(checked);
100 | };
101 |
102 | return (
103 |
111 |
112 |
117 |
126 |
127 |
134 |
141 |
148 |
152 |
161 |
170 |
179 |
180 |
181 |
187 |
188 |
189 |
198 |
199 | )
200 | }
201 |
202 | export default App;
203 |
--------------------------------------------------------------------------------
/src/Constants.tsx:
--------------------------------------------------------------------------------
1 | const ASSUMED_BACKGROUND_COLOR = [255, 255, 255];
2 | const DEFAULTBGCOLOR = "#ffffff";
3 | const DEFAULTTEXTCOLOR = "hsl(0, 0%, 0%)";
4 | const DEFAULTLINKCOLOR = "rgba(0, 0, 255, 1)";
5 |
6 | export {
7 | ASSUMED_BACKGROUND_COLOR,
8 | DEFAULTBGCOLOR,
9 | DEFAULTTEXTCOLOR,
10 | DEFAULTLINKCOLOR,
11 | };
12 |
--------------------------------------------------------------------------------
/src/arrow-left.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/arrow-right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/color.d.ts:
--------------------------------------------------------------------------------
1 | type ColorObject = {
2 | userValue: string;
3 | type: string;
4 | alpha: boolean;
5 | rgb: Array;
6 | hex: string;
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/ColorInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { typeOfColor } from "../utils/type-of-color";
3 |
4 | type ColorInputProps = {
5 | defaultValue: string;
6 | hex?: string;
7 | label: string;
8 | className: string;
9 | onChange: Function;
10 | };
11 |
12 | const ColorInput: React.FC = props => {
13 | const { defaultValue, hex, label, className, onChange } = props;
14 |
15 | const [value, setValue] = useState(defaultValue);
16 |
17 | useEffect(() => {
18 | if (typeOfColor(value) !== undefined) {
19 | onChange(value);
20 | }
21 | }, [onChange, value]);
22 |
23 | return (
24 |
25 |
26 | {label}
27 |
28 | setValue(e.target.value)}
32 | placeholder={defaultValue}
33 | title={`Enter ${label} as a string`}
34 | />
35 | setValue(e.target.value)}
39 | placeholder={defaultValue}
40 | title={`Enter ${label} with a color picker`}
41 | />
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default ColorInput;
49 |
--------------------------------------------------------------------------------
/src/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | type FooterProps = {
4 | textColor: string;
5 | linkColor: string;
6 | textDecoration: string;
7 | };
8 |
9 | const Footer: React.FC = props => {
10 | const { textColor, linkColor, textDecoration } = props;
11 | return (
12 |
44 | );
45 | };
46 |
47 | export default Footer;
48 |
--------------------------------------------------------------------------------
/src/components/PreviewParagraph.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | type PreviewParagraphProps = {
4 | textColor: string;
5 | linkColor: string;
6 | textDecoration: string;
7 | };
8 |
9 | const PreviewParagraph: React.FC = props => {
10 | const { textColor, linkColor, textDecoration } = props;
11 |
12 | return (
13 |
14 | Removing underlines from links in HTML text presents an accessibility
15 | challenge. In order for a design to be{" "}
16 |
23 | considered accessible
24 |
25 | , there is now a three-sided design contraint - or what I call "The
26 | Contrast Triangle". Your text, links and background colors must now{" "}
27 | all have sufficient contrast from each other .{" "}
28 |
35 | Links must have a contrast ratio of 3:1 from their surrounding text.
36 | {" "}
37 | This is so that colorblind users can tell what pieces of text are links.
38 | By not using underlines, a design has to rely on contrast alone to achieve
39 | this. Even the default blue link color in browsers doesn't meet this
40 | contrast level.{" "}
41 |
48 | Both the text and links have to have a contrast ratio of 4.5:1, or 3:1
49 | if it's large scale text.
50 |
51 |
52 | );
53 | };
54 |
55 | export default PreviewParagraph;
56 |
--------------------------------------------------------------------------------
/src/components/ResultCard.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { calculateContrast } from "../utils/calculate-contrast";
3 | import ResultEmoji from "./ResultEmoji";
4 |
5 | type ResultCardProps = {
6 | color1: Array;
7 | color2: Array;
8 | min: number;
9 | warn?: number;
10 | textDecoration?: string;
11 | label1: string;
12 | label2: string;
13 | className: string;
14 | };
15 |
16 | const ResultCard: React.FC = props => {
17 | const {
18 | color1,
19 | color2,
20 | min,
21 | warn,
22 | textDecoration,
23 | label1,
24 | label2,
25 | className
26 | } = props;
27 |
28 | const contrast = calculateContrast(color1, color2);
29 |
30 | const opacityStyle = () => {
31 | if (textDecoration === "underline") {
32 | return 0.2;
33 | } else {
34 | return 1;
35 | }
36 | };
37 |
38 | return (
39 |
43 |
44 |
45 | {label1}:{label2}
46 |
47 |
48 |
{contrast}:1
49 |
50 |
51 |
52 |
53 | );
54 | };
55 |
56 | export default ResultCard;
57 |
--------------------------------------------------------------------------------
/src/components/ResultEmoji.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | type ResultEmojiProps = {
4 | contrast: number;
5 | min: number;
6 | warn?: number;
7 | };
8 |
9 | const ResultEmoji: React.FC = props => {
10 | const { contrast, min, warn } = props;
11 |
12 | let emoji = `❌`;
13 |
14 | if (contrast >= min) {
15 | emoji = `✅`;
16 | } else if (warn && contrast >= warn) {
17 | emoji = `⚠️`;
18 | }
19 |
20 | let helptext = `Need ${min}:1 to pass`;
21 |
22 | if (warn) {
23 | helptext += `, or ${warn}:1 at large sizes`;
24 | }
25 |
26 | return (
27 |
28 | {emoji}
29 |
30 | );
31 | };
32 |
33 | export default ResultEmoji;
34 |
--------------------------------------------------------------------------------
/src/components/Results.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { calculateContrast } from "../utils/calculate-contrast";
3 | import ResultEmoji from "./ResultEmoji";
4 |
5 | type ResultsProps = {
6 | bgColor: Array;
7 | textColor: Array;
8 | linkColor: Array;
9 | textDecoration: string;
10 | };
11 |
12 | const Results: React.FC = props => {
13 | const { bgColor, textColor, linkColor, textDecoration } = props;
14 | const textBgContrast = calculateContrast(textColor, bgColor);
15 | const linkBgContrast = calculateContrast(linkColor, bgColor);
16 | const textLinkContrast = calculateContrast(textColor, linkColor);
17 |
18 | const strikeStyle = () => {
19 | if (textDecoration === "underline") {
20 | return `line-through`;
21 | } else {
22 | return `none`;
23 | }
24 | };
25 |
26 | return (
27 |
28 |
29 | Text :
30 | Background contrast is {textBgContrast}
31 | (Needed: 3 for large text, 4.5 for normal)
32 |
33 |
34 | Link :
35 | Background contrast is {linkBgContrast}
36 | (Needed: 3 for large text, 4.5 for normal)
37 |
38 |
43 | Text : Link contrast
44 | is {textLinkContrast}
45 | (Needed: 3 if underlines are absent)
46 |
47 |
48 | );
49 | };
50 |
51 | export default Results;
52 |
--------------------------------------------------------------------------------
/src/components/UnderlineControl.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | type UnderlineControlProps = {
4 | onChange: Function;
5 | textDecoration: string;
6 | };
7 |
8 | const UnderlineControl: React.FC = props => {
9 | const { onChange, textDecoration } = props;
10 |
11 | const initChecked = textDecoration === `underlines` ? true : false;
12 |
13 | const [checked, setChecked] = useState(initChecked);
14 |
15 | const handleClick = (checked: boolean) => {
16 | setChecked(!checked);
17 | onChange(!checked);
18 | }
19 |
20 | return (
21 |
22 |
Show underlines:
23 | handleClick(checked)}
27 | className="underline-control__toggle"
28 | aria-label={`Toggle underlines ${checked ? `Off` : `On`}`}
29 | >
30 | On
31 | Off
32 |
33 |
34 | );
35 | };
36 |
37 | export default UnderlineControl;
38 |
--------------------------------------------------------------------------------
/src/contrast-triangle-logo.svg:
--------------------------------------------------------------------------------
1 |
2 | The Contrast Triangle
3 |
4 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render( , document.getElementById('root'));
8 |
9 | // If you want your app to work offline and load faster, you can change
10 | // unregister() to register() below. Note this comes with some pitfalls.
11 | // Learn more about service workers: https://bit.ly/CRA-PWA
12 | serviceWorker.unregister();
13 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | type Config = {
24 | onSuccess?: (registration: ServiceWorkerRegistration) => void;
25 | onUpdate?: (registration: ServiceWorkerRegistration) => void;
26 | };
27 |
28 | export function register(config?: Config) {
29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
30 | // The URL constructor is available in all browsers that support SW.
31 | const publicUrl = new URL(
32 | process.env.PUBLIC_URL,
33 | window.location.href
34 | );
35 | if (publicUrl.origin !== window.location.origin) {
36 | // Our service worker won't work if PUBLIC_URL is on a different origin
37 | // from what our page is served on. This might happen if a CDN is used to
38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
39 | return;
40 | }
41 |
42 | window.addEventListener('load', () => {
43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
44 |
45 | if (isLocalhost) {
46 | // This is running on localhost. Let's check if a service worker still exists or not.
47 | checkValidServiceWorker(swUrl, config);
48 |
49 | // Add some additional logging to localhost, pointing developers to the
50 | // service worker/PWA documentation.
51 | navigator.serviceWorker.ready.then(() => {
52 | console.log(
53 | 'This web app is being served cache-first by a service ' +
54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
55 | );
56 | });
57 | } else {
58 | // Is not localhost. Just register service worker
59 | registerValidSW(swUrl, config);
60 | }
61 | });
62 | }
63 | }
64 |
65 | function registerValidSW(swUrl: string, config?: Config) {
66 | navigator.serviceWorker
67 | .register(swUrl)
68 | .then(registration => {
69 | registration.onupdatefound = () => {
70 | const installingWorker = registration.installing;
71 | if (installingWorker == null) {
72 | return;
73 | }
74 | installingWorker.onstatechange = () => {
75 | if (installingWorker.state === 'installed') {
76 | if (navigator.serviceWorker.controller) {
77 | // At this point, the updated precached content has been fetched,
78 | // but the previous service worker will still serve the older
79 | // content until all client tabs are closed.
80 | console.log(
81 | 'New content is available and will be used when all ' +
82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
83 | );
84 |
85 | // Execute callback
86 | if (config && config.onUpdate) {
87 | config.onUpdate(registration);
88 | }
89 | } else {
90 | // At this point, everything has been precached.
91 | // It's the perfect time to display a
92 | // "Content is cached for offline use." message.
93 | console.log('Content is cached for offline use.');
94 |
95 | // Execute callback
96 | if (config && config.onSuccess) {
97 | config.onSuccess(registration);
98 | }
99 | }
100 | }
101 | };
102 | };
103 | })
104 | .catch(error => {
105 | console.error('Error during service worker registration:', error);
106 | });
107 | }
108 |
109 | function checkValidServiceWorker(swUrl: string, config?: Config) {
110 | // Check if the service worker can be found. If it can't reload the page.
111 | fetch(swUrl, {
112 | headers: { 'Service-Worker': 'script' }
113 | })
114 | .then(response => {
115 | // Ensure service worker exists, and that we really are getting a JS file.
116 | const contentType = response.headers.get('content-type');
117 | if (
118 | response.status === 404 ||
119 | (contentType != null && contentType.indexOf('javascript') === -1)
120 | ) {
121 | // No service worker found. Probably a different app. Reload the page.
122 | navigator.serviceWorker.ready.then(registration => {
123 | registration.unregister().then(() => {
124 | window.location.reload();
125 | });
126 | });
127 | } else {
128 | // Service worker found. Proceed as normal.
129 | registerValidSW(swUrl, config);
130 | }
131 | })
132 | .catch(() => {
133 | console.log(
134 | 'No internet connection found. App is running in offline mode.'
135 | );
136 | });
137 | }
138 |
139 | export function unregister() {
140 | if ('serviceWorker' in navigator) {
141 | navigator.serviceWorker.ready.then(registration => {
142 | registration.unregister();
143 | });
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/src/utils/calculate-contrast.test.ts:
--------------------------------------------------------------------------------
1 | import { calculateContrast } from "./calculate-contrast";
2 |
3 | describe("Contrast Calculations", () => {
4 | it("correct contrast calculation black, white", () => {
5 | expect(calculateContrast([0, 0, 0], [255, 255, 255])).toBe(21);
6 | });
7 |
8 | it("correct contrast calculation black, black", () => {
9 | expect(calculateContrast([0, 0, 0], [0, 0, 0])).toBe(1);
10 | });
11 |
12 | it("correct contrast calculation black, blue", () => {
13 | expect(calculateContrast([0, 0, 0], [0, 0, 255])).toBe(2.44);
14 | });
15 |
16 | it("correct contrast calculation black, red", () => {
17 | expect(calculateContrast([0, 0, 0], [255, 0, 0])).toBe(5.25);
18 | });
19 |
20 | it("correct contrast calculation black, hotpink", () => {
21 | expect(calculateContrast([0, 0, 0], [255, 105, 180])).toBe(7.93);
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/utils/calculate-contrast.tsx:
--------------------------------------------------------------------------------
1 | import { getLuminance } from "./get-luminance";
2 |
3 | const calculateContrast = (color1:Array, color2:Array): number => {
4 | const l1 = getLuminance(color1[0], color1[1], color1[2]);
5 | const l2 = getLuminance(color2[0], color2[1], color2[2]);
6 |
7 | let contrast = 0;
8 |
9 | // the contrast calculation depends on which is lighter
10 | if (l1 > l2) {
11 | contrast = (l1 + 0.05) / (l2 + 0.05);
12 | } else {
13 | contrast = (l2 + 0.05) / (l1 + 0.05);
14 | }
15 |
16 | return parseFloat(contrast.toFixed(2));
17 | };
18 |
19 | export { calculateContrast };
20 |
--------------------------------------------------------------------------------
/src/utils/calculate-overlay.test.ts:
--------------------------------------------------------------------------------
1 | import { calculateOverlay } from "./calculate-overlay";
2 |
3 | describe("Calculate Overlay", () => {
4 | // not sure if this is acurate
5 | it("returns a value for white at 80%", () => {
6 | expect(calculateOverlay([255, 255, 255, 0.8], [0, 0, 0])).toEqual([
7 | 204,
8 | 204,
9 | 204
10 | ]);
11 | });
12 |
13 | it("returns the correct value for black at 50% alpha", () => {
14 | expect(calculateOverlay([0, 0, 0, 0.5], [255, 255, 255])).toEqual([
15 | 127,
16 | 127,
17 | 127
18 | ]);
19 | });
20 |
21 | it("returns the correct value for red at 50% alpha", () => {
22 | expect(calculateOverlay([255, 0, 0, 0.5], [255, 255, 255])).toEqual([
23 | 255,
24 | 127,
25 | 127
26 | ]);
27 | });
28 |
29 | it("returns the correct value for a blue at 20% alpha", () => {
30 | expect(calculateOverlay([0, 0, 255, 0.2], [255, 255, 255])).toEqual([
31 | 204,
32 | 204,
33 | 255
34 | ]);
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/src/utils/calculate-overlay.tsx:
--------------------------------------------------------------------------------
1 | const channelResult = (fg: number, bg: number, alpha:number): number => {
2 | return Math.floor(alpha * fg + (1 - alpha) * bg);
3 | };
4 |
5 | const calculateOverlay = (foreGroundRgba: Array, backgroundRgb: Array): Array => {
6 | const fgR = foreGroundRgba[0];
7 | const fgG = foreGroundRgba[1];
8 | const fgB = foreGroundRgba[2];
9 | const fgA = foreGroundRgba[3];
10 |
11 | const bgR = backgroundRgb[0];
12 | const bgG = backgroundRgb[1];
13 | const bgB = backgroundRgb[2];
14 |
15 | const r = channelResult(fgR, bgR, fgA);
16 | const g = channelResult(fgG, bgG, fgA);
17 | const b = channelResult(fgB, bgB, fgA);
18 |
19 | return [+r, +g, +b];
20 | };
21 |
22 | export { calculateOverlay };
23 |
--------------------------------------------------------------------------------
/src/utils/check-yourself-before-you-hex-yourself.test.ts:
--------------------------------------------------------------------------------
1 | import { checkYourSelfBeforeYouHexYourself } from "./check-yourself-before-you-hex-yourself";
2 |
3 | describe("Check Yourself Before You Hex Yourself", () => {
4 | it("returns correct value for black", () => {
5 | expect(checkYourSelfBeforeYouHexYourself("hsl(0, 0%, 0%)")).toBe(
6 | "hsl(0, 0%, 0%)"
7 | );
8 | expect(checkYourSelfBeforeYouHexYourself("000000")).toBe("#000000");
9 | expect(checkYourSelfBeforeYouHexYourself("#000000")).toBe("#000000");
10 | });
11 |
12 | it("returns correct value for white", () => {
13 | expect(checkYourSelfBeforeYouHexYourself("hsl(0, 0%, 100%)")).toBe(
14 | "hsl(0, 0%, 100%)"
15 | );
16 | expect(checkYourSelfBeforeYouHexYourself("fff")).toBe("#fff");
17 | expect(checkYourSelfBeforeYouHexYourself("#fff")).toBe("#fff");
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/utils/check-yourself-before-you-hex-yourself.tsx:
--------------------------------------------------------------------------------
1 | import { typeOfColor } from "./type-of-color";
2 |
3 | // this just normalizes a hex color by making sure to insert
4 | // an octothorp (#) if the color is a valid hex, but lacks
5 | // the octothorp. this is needed for passing hex values to
6 | // the color inputs, which need a # for the value passed to them
7 | const checkYourSelfBeforeYouHexYourself = (color: string): string => {
8 | if (typeOfColor(color)?.indexOf("hex") !== -1 && color.indexOf("#") !== 0) {
9 | return `#${color}`;
10 | } else {
11 | return color;
12 | }
13 | };
14 |
15 | export { checkYourSelfBeforeYouHexYourself };
16 |
--------------------------------------------------------------------------------
/src/utils/color-translate.test.ts:
--------------------------------------------------------------------------------
1 | import { colorTranslate } from "./color-translate";
2 |
3 | describe("Color Translate", () => {
4 |
5 | it("returns result from hsl", () => {
6 | expect(colorTranslate("hsl(30, 100%, 50%)", [0, 0, 0], true)).toEqual({
7 | alpha: false,
8 | hex: "#ff8000",
9 | rgb: [255, 128, 0],
10 | type: "hsl",
11 | userValue: "hsl(30, 100%, 50%)"
12 | });
13 | });
14 |
15 | it("returns result for HSLA background", () => {
16 | expect(
17 | colorTranslate("hsla(30, 100%, 50%, .8)", [0, 0, 0], true)
18 | ).toEqual({
19 | alpha: true,
20 | hex: "#ff9932",
21 | rgb: [255, 153, 50],
22 | type: "hsla",
23 | userValue: "hsla(30, 100%, 50%, .8)"
24 | });
25 | });
26 |
27 | it("returns result for HSLA text color", () => {
28 | expect(
29 | colorTranslate("hsla(30, 100%, 50%, .8)", [0, 0, 0])
30 | ).toEqual({
31 | alpha: true,
32 | hex: "#cc6600",
33 | rgb: [204, 102, 0],
34 | type: "hsla",
35 | userValue: "hsla(30, 100%, 50%, .8)"
36 | });
37 | });
38 |
39 | it("returns result for hex text color", () => {
40 | expect(colorTranslate("#badbad", [0, 0, 0])).toEqual({
41 | alpha: false,
42 | hex: "#badbad",
43 | rgb: [186, 219, 173],
44 | type: "hex6",
45 | userValue: "#badbad"
46 | });
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/src/utils/color-translate.tsx:
--------------------------------------------------------------------------------
1 | import { toRgb } from "./to-rgb";
2 | import { toRgba } from "./to-rgba";
3 | import { rgbArrayToHex, toHex } from "./to-hex";
4 | import { typeOfColor } from "./type-of-color";
5 | import { calculateOverlay } from "./calculate-overlay";
6 | import { ASSUMED_BACKGROUND_COLOR } from "../Constants";
7 |
8 | const colorTranslate = (
9 | color: string,
10 | bgColorRgb: Array,
11 | isBackground: boolean = false ): ColorObject => {
12 |
13 | const colorType = typeOfColor(color);
14 |
15 | const result = {} as ColorObject;
16 |
17 | result.userValue = color;
18 | result.type = colorType;
19 |
20 | const colorTypesWithAlpha = ["rgba", "hsla", "hex4", "hex8"];
21 | const isAlpha = colorTypesWithAlpha.includes(colorType);
22 | result.alpha = isAlpha;
23 |
24 | const colorAsRgba = toRgba(color);
25 | // baked in assumption of white behind the background
26 | const bgRgb = isBackground ? ASSUMED_BACKGROUND_COLOR : bgColorRgb;
27 |
28 | // if we have a transparent color
29 | if (isAlpha && colorAsRgba) {
30 | const overlayRgb = calculateOverlay(colorAsRgba, bgRgb);
31 | result.rgb = overlayRgb;
32 | result.hex = rgbArrayToHex(overlayRgb);
33 | } else {
34 | result.rgb = toRgb(color);
35 | result.hex = toHex(color);
36 | }
37 |
38 | return result;
39 | };
40 |
41 | export { colorTranslate };
42 |
--------------------------------------------------------------------------------
/src/utils/get-luminance.test.ts:
--------------------------------------------------------------------------------
1 | import { getLuminance } from "./get-luminance";
2 |
3 | describe("Luminance Calculations", () => {
4 | it("correct luminance for black", () => {
5 | expect(getLuminance(0, 0, 0)).toBe(0);
6 | });
7 |
8 | it("correct luminance for white", () => {
9 | expect(getLuminance(255, 255, 255)).toBe(1);
10 | });
11 |
12 | it("correct luminance for hotpink", () => {
13 | expect(getLuminance(255, 105, 180)).toBe(0.3465843816971475);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/utils/get-luminance.tsx:
--------------------------------------------------------------------------------
1 | // https://gist.github.com/jfsiii/5641126
2 | const getLuminance = (r: number, g: number, b: number): number => {
3 | const RsRGB = r / 255;
4 | const GsRGB = g / 255;
5 | const BsRGB = b / 255;
6 |
7 | var R =
8 | RsRGB <= 0.03928 ? RsRGB / 12.92 : Math.pow((RsRGB + 0.055) / 1.055, 2.4);
9 | var G =
10 | GsRGB <= 0.03928 ? GsRGB / 12.92 : Math.pow((GsRGB + 0.055) / 1.055, 2.4);
11 | var B =
12 | BsRGB <= 0.03928 ? BsRGB / 12.92 : Math.pow((BsRGB + 0.055) / 1.055, 2.4);
13 |
14 | // For the sRGB colorspace, the relative luminance of a color is defined as:
15 | var L = 0.2126 * R + 0.7152 * G + 0.0722 * B;
16 |
17 | return L;
18 | };
19 |
20 | export { getLuminance };
21 |
--------------------------------------------------------------------------------
/src/utils/named-colors.tsx:
--------------------------------------------------------------------------------
1 | // taken from this github gist comment
2 | // https://gist.github.com/bobspace/2712980#gistcomment-2688195
3 | const namedColors = [
4 | "AliceBlue",
5 | "AntiqueWhite",
6 | "Aqua",
7 | "Aquamarine",
8 | "Azure",
9 | "Beige",
10 | "Bisque",
11 | "Black",
12 | "BlanchedAlmond",
13 | "Blue",
14 | "BlueViolet",
15 | "Brown",
16 | "BurlyWood",
17 | "CadetBlue",
18 | "Chartreuse",
19 | "Chocolate",
20 | "Coral",
21 | "CornflowerBlue",
22 | "Cornsilk",
23 | "Crimson",
24 | "Cyan",
25 | "DarkBlue",
26 | "DarkCyan",
27 | "DarkGoldenRod",
28 | "DarkGray",
29 | "DarkGrey",
30 | "DarkGreen",
31 | "DarkKhaki",
32 | "DarkMagenta",
33 | "DarkOliveGreen",
34 | "DarkOrange",
35 | "DarkOrchid",
36 | "DarkRed",
37 | "DarkSalmon",
38 | "DarkSeaGreen",
39 | "DarkSlateBlue",
40 | "DarkSlateGray",
41 | "DarkSlateGrey",
42 | "DarkTurquoise",
43 | "DarkViolet",
44 | "DeepPink",
45 | "DeepSkyBlue",
46 | "DimGray",
47 | "DimGrey",
48 | "DodgerBlue",
49 | "FireBrick",
50 | "FloralWhite",
51 | "ForestGreen",
52 | "Fuchsia",
53 | "Gainsboro",
54 | "GhostWhite",
55 | "Gold",
56 | "GoldenRod",
57 | "Gray",
58 | "Grey",
59 | "Green",
60 | "GreenYellow",
61 | "HoneyDew",
62 | "HotPink",
63 | "IndianRed",
64 | "Indigo",
65 | "Ivory",
66 | "Khaki",
67 | "Lavender",
68 | "LavenderBlush",
69 | "LawnGreen",
70 | "LemonChiffon",
71 | "LightBlue",
72 | "LightCoral",
73 | "LightCyan",
74 | "LightGoldenRodYellow",
75 | "LightGray",
76 | "LightGrey",
77 | "LightGreen",
78 | "LightPink",
79 | "LightSalmon",
80 | "LightSeaGreen",
81 | "LightSkyBlue",
82 | "LightSlateGray",
83 | "LightSlateGrey",
84 | "LightSteelBlue",
85 | "LightYellow",
86 | "Lime",
87 | "LimeGreen",
88 | "Linen",
89 | "Magenta",
90 | "Maroon",
91 | "MediumAquaMarine",
92 | "MediumBlue",
93 | "MediumOrchid",
94 | "MediumPurple",
95 | "MediumSeaGreen",
96 | "MediumSlateBlue",
97 | "MediumSpringGreen",
98 | "MediumTurquoise",
99 | "MediumVioletRed",
100 | "MidnightBlue",
101 | "MintCream",
102 | "MistyRose",
103 | "Moccasin",
104 | "NavajoWhite",
105 | "Navy",
106 | "OldLace",
107 | "Olive",
108 | "OliveDrab",
109 | "Orange",
110 | "OrangeRed",
111 | "Orchid",
112 | "PaleGoldenRod",
113 | "PaleGreen",
114 | "PaleTurquoise",
115 | "PaleVioletRed",
116 | "PapayaWhip",
117 | "PeachPuff",
118 | "Peru",
119 | "Pink",
120 | "Plum",
121 | "PowderBlue",
122 | "Purple",
123 | "RebeccaPurple",
124 | "Red",
125 | "RosyBrown",
126 | "RoyalBlue",
127 | "SaddleBrown",
128 | "Salmon",
129 | "SandyBrown",
130 | "SeaGreen",
131 | "SeaShell",
132 | "Sienna",
133 | "Silver",
134 | "SkyBlue",
135 | "SlateBlue",
136 | "SlateGray",
137 | "SlateGrey",
138 | "Snow",
139 | "SpringGreen",
140 | "SteelBlue",
141 | "Tan",
142 | "Teal",
143 | "Thistle",
144 | "Tomato",
145 | "Turquoise",
146 | "Violet",
147 | "Wheat",
148 | "White",
149 | "WhiteSmoke",
150 | "Yellow",
151 | "YellowGreen"
152 | ];
153 |
154 | export { namedColors };
155 |
--------------------------------------------------------------------------------
/src/utils/queryString.tsx:
--------------------------------------------------------------------------------
1 | import qs from "query-string";
2 |
3 | // taken from
4 | // https://medium.com/swlh/using-react-hooks-to-sync-your-component-state-with-the-url-query-string-81ccdfcb174f
5 |
6 | // https://codesandbox.io/embed/boring-butterfly-pvmi9?fontsize=14
7 |
8 | const setQueryStringWithoutPageReload = (qsValue:string) => {
9 | const newurl =
10 | window.location.protocol +
11 | "//" +
12 | window.location.host +
13 | window.location.pathname +
14 | qsValue;
15 | window.history.pushState({ path: newurl }, "", newurl);
16 | };
17 |
18 | export const getQueryStringValue = (
19 | key:string,
20 | queryString = window.location.search
21 | ) => {
22 | const values = qs.parse(queryString);
23 | return values[key];
24 | };
25 |
26 | export const setQueryStringValue = (
27 | key:string,
28 | value:string,
29 | queryString = window.location.search
30 | ) => {
31 | const values = qs.parse(queryString);
32 | const newQsValue = qs.stringify({
33 | ...values,
34 | [key]: value
35 | });
36 | setQueryStringWithoutPageReload(`?${newQsValue}`);
37 | };
38 |
--------------------------------------------------------------------------------
/src/utils/to-hex.test.ts:
--------------------------------------------------------------------------------
1 | import { hexToHex, hslToHex, rgbToHex, toHex } from "./to-hex";
2 |
3 | describe("Hex To Hex conversion", () => {
4 | it("correct hex for black", () => {
5 | expect(hexToHex("#000")).toEqual("#000000");
6 | expect(hexToHex("000")).toEqual("#000000");
7 | expect(hexToHex("#000000")).toEqual("#000000");
8 | expect(hexToHex("000000")).toEqual("#000000");
9 | });
10 |
11 | it("correct hex for white", () => {
12 | expect(hexToHex("#fff")).toEqual("#ffffff");
13 | expect(hexToHex("fff")).toEqual("#ffffff");
14 | expect(hexToHex("#ffffff")).toEqual("#ffffff");
15 | expect(hexToHex("ffffff")).toEqual("#ffffff");
16 | });
17 | });
18 |
19 | describe("HSL to Hex conversion", () => {
20 | it("correct Hex for black", () => {
21 | expect(hslToHex("hsl(0, 0%, 0%)")).toBe("#000000");
22 | });
23 |
24 | it("correct Hex for white", () => {
25 | expect(hslToHex("hsl(0, 0%, 100%)")).toBe("#ffffff");
26 | });
27 |
28 | it("correct Hex for orange", () => {
29 | expect(hslToHex("hsl(30, 100%, 50%)")).toEqual("#ff8000");
30 | })
31 | });
32 |
33 | describe("RGB to Hex conversion", () => {
34 | it("correct Hex for black", () => {
35 | expect(rgbToHex("rgb(0, 0, 0)")).toBe("#000000");
36 | });
37 |
38 | it("correct Hex for white", () => {
39 | expect(rgbToHex("rgb(255, 255, 255)")).toBe("#ffffff");
40 | });
41 | });
42 |
43 | // integration
44 | describe("To Hex conversion", () => {
45 | it("correct hex for black", () => {
46 | expect(toHex("hsl(0, 0%, 0%)")).toEqual("#000000");
47 | expect(toHex("rgb(0, 0, 0)")).toEqual("#000000");
48 | expect(toHex("#000")).toEqual("#000000");
49 | expect(toHex("000")).toEqual("#000000");
50 | expect(toHex("#000000")).toEqual("#000000");
51 | expect(toHex("000000")).toEqual("#000000");
52 | });
53 |
54 | it("correct hex for white", () => {
55 | expect(toHex("hsl(0, 0%, 100%)")).toEqual("#ffffff");
56 | expect(toHex("rgb(255, 255, 255)")).toEqual("#ffffff");
57 | expect(toHex("#fff")).toEqual("#ffffff");
58 | expect(toHex("fff")).toEqual("#ffffff");
59 | expect(toHex("#ffffff")).toEqual("#ffffff");
60 | expect(toHex("ffffff")).toEqual("#ffffff");
61 | });
62 |
63 | it("correct hex for orange", () => {
64 | expect(toHex("hsl(30, 100%, 50%)")).toEqual("#ff8000");
65 | })
66 | });
67 |
--------------------------------------------------------------------------------
/src/utils/to-hex.ts:
--------------------------------------------------------------------------------
1 | import { typeOfColor } from "./type-of-color";
2 |
3 | // normalizes non-alpha hex values
4 | // handles 000, #000, 000000 or #000000
5 | const hexToHex = (hex: string): string => {
6 | let result = "";
7 |
8 | // fff
9 | if (hex.length === 3) {
10 | result = `#${hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]}`;
11 | } else if (hex.length === 4) {
12 | // #fff
13 | result = `#${hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3]}`;
14 | } else if (hex.length === 6) {
15 | // ffffff
16 | result = `#${hex[0] + hex[1] + hex[2] + hex[3] + hex[4] + hex[5]}`;
17 | } else if (hex.length === 7) {
18 | // #ffffff
19 | result = `#${hex[1] + hex[2] + hex[3] + hex[4] + hex[5] + hex[6]}`;
20 | }
21 |
22 | return result;
23 | };
24 |
25 | // based on https://css-tricks.com/converting-color-spaces-in-javascript/#article-header-id-19
26 | const hslToHex = (hsl: string): string => {
27 | const sep = hsl.indexOf(",") > -1 ? "," : " ";
28 |
29 | let hslArray: Array = hsl
30 | .substr(4)
31 | .split(")")[0]
32 | .split(sep);
33 |
34 | let h = hslArray[0]; // leaving this a string for now
35 | let s = parseInt(hslArray[1].substr(0, hslArray[1].length - 1)) / 100 || 0;
36 | let l = parseInt(hslArray[2].substr(0, hslArray[2].length - 1)) / 100 || 0;
37 |
38 | let hNum: number;
39 |
40 | // Strip label and convert to degrees (if necessary)
41 | if (h.indexOf("deg") > -1) {
42 | hNum = parseInt(h.substr(0, h.length - 3));
43 | } else if (h.indexOf("rad") > -1) {
44 | hNum = Math.round(parseInt(h.substr(0, h.length - 3)) * (180 / Math.PI));
45 | } else if (h.indexOf("turn") > -1) {
46 | hNum = Math.round(parseInt(h.substr(0, h.length - 4)) * 360);
47 | } else {
48 | hNum = parseInt(h);
49 | }
50 |
51 | // Keep hue fraction of 360 if ending up over
52 | if (hNum >= 360) {
53 | hNum %= 360;
54 | }
55 |
56 | let c = (1 - Math.abs(2 * l - 1)) * s;
57 | let x = c * (1 - Math.abs(((hNum / 60) % 2) - 1));
58 | let m = l - c / 2;
59 | let r = 0;
60 | let g = 0;
61 | let b = 0;
62 |
63 | if (0 <= hNum && hNum < 60) {
64 | r = c;
65 | g = x;
66 | b = 0;
67 | } else if (60 <= hNum && hNum < 120) {
68 | r = x;
69 | g = c;
70 | b = 0;
71 | } else if (120 <= hNum && hNum < 180) {
72 | r = 0;
73 | g = c;
74 | b = x;
75 | } else if (180 <= hNum && hNum < 240) {
76 | r = 0;
77 | g = x;
78 | b = c;
79 | } else if (240 <= hNum && hNum < 300) {
80 | r = x;
81 | g = 0;
82 | b = c;
83 | } else if (300 <= hNum && hNum < 360) {
84 | r = c;
85 | g = 0;
86 | b = x;
87 | }
88 | // Having obtained RGB, convert channels to hex
89 | let rHex = Math.round((r + m) * 255).toString(16);
90 | let gHex = Math.round((g + m) * 255).toString(16);
91 | let bHex = Math.round((b + m) * 255).toString(16);
92 |
93 | // Prepend 0s, if necessary
94 | if (rHex.length === 1) rHex = "0" + rHex;
95 | if (gHex.length === 1) gHex = "0" + gHex;
96 | if (bHex.length === 1) bHex = "0" + bHex;
97 |
98 | return "#" + rHex + gHex + bHex;
99 | };
100 |
101 | // https://css-tricks.com/converting-color-spaces-in-javascript/#article-header-id-1
102 | const rgbToHex = (rgb: string): string => {
103 | let sep = rgb.indexOf(",") > -1 ? "," : " ";
104 |
105 | // Turn "rgb(r,g,b)" into [r,g,b]
106 | const rgbArray: Array = rgb
107 | .substr(4)
108 | .split(")")[0]
109 | .split(sep);
110 |
111 | let r = (+rgbArray[0]).toString(16);
112 | let g = (+rgbArray[1]).toString(16);
113 | let b = (+rgbArray[2]).toString(16);
114 |
115 | if (r.length === 1) r = "0" + r;
116 | if (g.length === 1) g = "0" + g;
117 | if (b.length === 1) b = "0" + b;
118 |
119 | return "#" + r + g + b;
120 | };
121 |
122 | const rgbArrayToHex = (rgb: Array) => {
123 | let r = (+Math.round(rgb[0])).toString(16),
124 | g = (+Math.round(rgb[1])).toString(16),
125 | b = (+Math.round(rgb[2])).toString(16);
126 |
127 | if (r.length === 1) r = "0" + r;
128 | if (g.length === 1) g = "0" + g;
129 | if (b.length === 1) b = "0" + b;
130 |
131 | return "#" + r + g + b;
132 | };
133 |
134 | const namedToHex = (name:string) => {
135 | // Create fake div
136 | let fakeDiv = document.createElement("div");
137 | fakeDiv.style.color = name;
138 | document.body.appendChild(fakeDiv);
139 |
140 | // Get color of div
141 | let cs = window.getComputedStyle(fakeDiv);
142 | let pv = cs.getPropertyValue("color");
143 |
144 | // Remove div after obtaining desired color value
145 | document.body.removeChild(fakeDiv);
146 | return rgbToHex(pv);
147 | };
148 |
149 | const toHex = (color:string) => {
150 | switch (true) {
151 | case typeOfColor(color) === "hex3":
152 | case typeOfColor(color) === "hex6":
153 | return hexToHex(color);
154 |
155 | case typeOfColor(color) === "rgb":
156 | return rgbToHex(color);
157 |
158 | case typeOfColor(color) === "hsl":
159 | return hslToHex(color);
160 |
161 | case typeOfColor(color) === "named":
162 | return namedToHex(color);
163 |
164 | default:
165 | // if nothing else, assume hex
166 | return hexToHex(color);
167 | }
168 | };
169 |
170 | export { hexToHex, hslToHex, rgbToHex, rgbArrayToHex, namedToHex, toHex };
171 |
--------------------------------------------------------------------------------
/src/utils/to-rgb.test.ts:
--------------------------------------------------------------------------------
1 | import { hexToRgb, hslToRgb, rgbToRgb, toRgb } from "./to-rgb";
2 |
3 | describe("Hex to RGB conversion", () => {
4 | it("correct rgb for black", () => {
5 | expect(hexToRgb("#000000")[0]).toBe(0);
6 | expect(hexToRgb("#000000")[1]).toBe(0);
7 | expect(hexToRgb("#000000")[2]).toBe(0);
8 |
9 | expect(hexToRgb("000000")[0]).toBe(0);
10 | expect(hexToRgb("000000")[1]).toBe(0);
11 | expect(hexToRgb("000000")[2]).toBe(0);
12 |
13 | expect(hexToRgb("#000")[0]).toBe(0);
14 | expect(hexToRgb("#000")[1]).toBe(0);
15 | expect(hexToRgb("#000")[2]).toBe(0);
16 |
17 | expect(hexToRgb("000")[0]).toBe(0);
18 | expect(hexToRgb("000")[1]).toBe(0);
19 | expect(hexToRgb("000")[2]).toBe(0);
20 | });
21 |
22 | it("correct rgb for white", () => {
23 | expect(hexToRgb("#ffffff")[0]).toBe(255);
24 | expect(hexToRgb("#ffffff")[1]).toBe(255);
25 | expect(hexToRgb("#ffffff")[2]).toBe(255);
26 |
27 | expect(hexToRgb("ffffff")[0]).toBe(255);
28 | expect(hexToRgb("ffffff")[1]).toBe(255);
29 | expect(hexToRgb("ffffff")[2]).toBe(255);
30 |
31 | expect(hexToRgb("#fff")[0]).toBe(255);
32 | expect(hexToRgb("#fff")[1]).toBe(255);
33 | expect(hexToRgb("#fff")[2]).toBe(255);
34 |
35 | expect(hexToRgb("fff")[0]).toBe(255);
36 | expect(hexToRgb("fff")[1]).toBe(255);
37 | expect(hexToRgb("fff")[2]).toBe(255);
38 | });
39 |
40 | it("correct rgb for hotpink", () => {
41 | expect(hexToRgb("#ff69b4")[0]).toBe(255);
42 | expect(hexToRgb("#ff69b4")[1]).toBe(105);
43 | expect(hexToRgb("#ff69b4")[2]).toBe(180);
44 | });
45 | });
46 |
47 | describe("HSL to RGB conversion", () => {
48 | it("correct rgb for black", () => {
49 | expect(hslToRgb("hsl(0, 0%, 0%)")[0]).toBe(0);
50 | expect(hslToRgb("hsl(0, 0%, 0%)")[1]).toBe(0);
51 | expect(hslToRgb("hsl(0, 0%, 0%)")[2]).toBe(0);
52 | });
53 |
54 | it("correct rgb for white", () => {
55 | expect(hslToRgb("hsl(0, 0%, 100%)")[0]).toBe(255);
56 | expect(hslToRgb("hsl(0, 0%, 100%)")[1]).toBe(255);
57 | expect(hslToRgb("hsl(0, 0%, 100%)")[2]).toBe(255);
58 | });
59 | });
60 |
61 | describe("rgb to RGB conversion", () => {
62 | it("correct rgb for black", () => {
63 | expect(rgbToRgb("rgb(0, 0, 0)")[0]).toBe(0);
64 | expect(rgbToRgb("rgb(0, 0, 0)")[1]).toBe(0);
65 | expect(rgbToRgb("rgb(0, 0, 0)")[2]).toBe(0);
66 | });
67 |
68 | it("correct rgb for white", () => {
69 | expect(rgbToRgb("rgb(255, 255, 255)")[0]).toBe(255);
70 | expect(rgbToRgb("rgb(255, 255, 255)")[1]).toBe(255);
71 | expect(rgbToRgb("rgb(255, 255, 255)")[2]).toBe(255);
72 | });
73 | });
74 |
75 | // @todo add enzyme or react testing library
76 | // so that styles can be calcuated
77 | // describe("named to RGB conversion", () => {
78 | // it("correct rgb for black", () => {
79 | // expect(namedToRgb("black")).toEqual([0, 0, 0]);
80 | // });
81 |
82 | // it("correct rgb for white", () => {
83 | // expect(namedToRgb("white")).toEqual([255, 255, 255]);
84 | // });
85 | // });
86 |
87 | // integration
88 | describe("To RGB conversion", () => {
89 | it("correct rgb for black", () => {
90 | expect(toRgb("hsl(0, 0%, 0%)")).toEqual([0, 0, 0]);
91 | expect(toRgb("rgb(0, 0, 0)")).toEqual([0, 0, 0]);
92 | expect(toRgb("#000")).toEqual([0, 0, 0]);
93 | expect(toRgb("000")).toEqual([0, 0, 0]);
94 | expect(toRgb("#000000")).toEqual([0, 0, 0]);
95 | expect(toRgb("000000")).toEqual([0, 0, 0]);
96 | // expect(toRgb("black")).toEqual([0, 0, 0]);
97 | });
98 |
99 | it("correct rgb for white", () => {
100 | expect(toRgb("hsl(0, 0%, 100%)")).toEqual([255, 255, 255]);
101 | expect(toRgb("rgb(255, 255, 255)")).toEqual([255, 255, 255]);
102 | expect(toRgb("#fff")).toEqual([255, 255, 255]);
103 | expect(toRgb("fff")).toEqual([255, 255, 255]);
104 | expect(toRgb("#ffffff")).toEqual([255, 255, 255]);
105 | expect(toRgb("ffffff")).toEqual([255, 255, 255]);
106 | });
107 | });
108 |
--------------------------------------------------------------------------------
/src/utils/to-rgb.ts:
--------------------------------------------------------------------------------
1 | import { typeOfColor } from "./type-of-color";
2 |
3 | // handles #000 or #000000
4 | // based on this function: https://css-tricks.com/converting-color-spaces-in-javascript/#article-header-id-3
5 | const hexToRgb = (hex: string): Array => {
6 | let r: number = 0;
7 | let g: number = 0;
8 | let b: number = 0;
9 |
10 | // 3 digits - fff
11 | if (hex.length === 3) {
12 | r = parseInt("0x" + hex[0] + hex[0]);
13 | g = parseInt("0x" + hex[1] + hex[1]);
14 | b = parseInt("0x" + hex[2] + hex[2]);
15 | } else if (hex.length === 4) {
16 | // #fff
17 | r = parseInt("0x" + hex[1] + hex[1]);
18 | g = parseInt("0x" + hex[2] + hex[2]);
19 | b = parseInt("0x" + hex[3] + hex[3]);
20 | } else if (hex.length === 6) {
21 | // ffffff
22 | r = parseInt("0x" + hex[0] + hex[1]);
23 | g = parseInt("0x" + hex[2] + hex[3]);
24 | b = parseInt("0x" + hex[4] + hex[5]);
25 | } else if (hex.length === 7) {
26 | // #ffffff
27 | r = parseInt("0x" + hex[1] + hex[2]);
28 | g = parseInt("0x" + hex[3] + hex[4]);
29 | b = parseInt("0x" + hex[5] + hex[6]);
30 | }
31 |
32 | return [+r, +g, +b];
33 | };
34 |
35 | const hslToRgb = (hsl: string): Array => {
36 | const sep = hsl.indexOf(",") > -1 ? "," : " ";
37 |
38 | let hslArray: Array = hsl
39 | .substr(4)
40 | .split(")")[0]
41 | .split(sep);
42 |
43 | let h = hslArray[0]; // leaving this a string for now
44 | let s = parseInt(hslArray[1].substr(0, hslArray[1].length - 1)) / 100 || 0;
45 | let l = parseInt(hslArray[2].substr(0, hslArray[2].length - 1)) / 100 || 0;
46 |
47 | let hNum: number;
48 |
49 | // Strip label and convert to degrees (if necessary)
50 | if (h.indexOf("deg") > -1) {
51 | hNum = parseInt(h.substr(0, h.length - 3));
52 | } else if (h.indexOf("rad") > -1) {
53 | hNum = Math.round(parseInt(h.substr(0, h.length - 3)) * (180 / Math.PI));
54 | } else if (h.indexOf("turn") > -1) {
55 | hNum = Math.round(parseInt(h.substr(0, h.length - 4)) * 360);
56 | } else {
57 | hNum = parseInt(h);
58 | }
59 |
60 | // Keep hue fraction of 360 if ending up over
61 | if (hNum >= 360) {
62 | hNum %= 360;
63 | }
64 |
65 | let c = (1 - Math.abs(2 * l - 1)) * s;
66 | let x = c * (1 - Math.abs(((hNum / 60) % 2) - 1));
67 | let m = l - c / 2;
68 | let r = 0;
69 | let g = 0;
70 | let b = 0;
71 | if (0 <= hNum && hNum < 60) {
72 | r = c;
73 | g = x;
74 | b = 0;
75 | } else if (60 <= hNum && hNum < 120) {
76 | r = x;
77 | g = c;
78 | b = 0;
79 | } else if (120 <= hNum && hNum < 180) {
80 | r = 0;
81 | g = c;
82 | b = x;
83 | } else if (180 <= hNum && hNum < 240) {
84 | r = 0;
85 | g = x;
86 | b = c;
87 | } else if (240 <= hNum && hNum < 300) {
88 | r = x;
89 | g = 0;
90 | b = c;
91 | } else if (300 <= hNum && hNum < 360) {
92 | r = c;
93 | g = 0;
94 | b = x;
95 | }
96 | r = Math.round((r + m) * 255);
97 | g = Math.round((g + m) * 255);
98 | b = Math.round((b + m) * 255);
99 |
100 | return [+r, +g, +b];
101 | };
102 |
103 | const rgbToRgb = (rgb: string): Array => {
104 | const sep = rgb.indexOf(",") > -1 ? "," : " ";
105 |
106 | const rgbArray = rgb
107 | .substr(4)
108 | .split(")")[0]
109 | .split(sep);
110 |
111 | const r = rgbArray[0];
112 | const g = rgbArray[1];
113 | const b = rgbArray[2];
114 |
115 | return [+r, +g, +b];
116 | };
117 |
118 | const namedToRgb = (name: string) => {
119 | // Create fake div
120 | let fakeDiv = document.createElement("div");
121 | fakeDiv.style.color = name;
122 | document.body.appendChild(fakeDiv);
123 |
124 | // Get color of div
125 | let cs = window.getComputedStyle(fakeDiv);
126 | let pv = cs.getPropertyValue("color");
127 |
128 | // Remove div after obtaining desired color value
129 | document.body.removeChild(fakeDiv);
130 | return rgbToRgb(pv);
131 | };
132 |
133 | const toRgb = (color: string): Array => {
134 | switch (true) {
135 | case typeOfColor(color) === "hex3":
136 | case typeOfColor(color) === "hex6":
137 | return hexToRgb(color);
138 |
139 | case typeOfColor(color) === "rgb":
140 | return rgbToRgb(color);
141 |
142 | case typeOfColor(color) === "hsl":
143 | return hslToRgb(color);
144 |
145 | case typeOfColor(color) === "named":
146 | return namedToRgb(color);
147 |
148 | default:
149 | // assume rgb to rgb
150 | return rgbToRgb(color);
151 | }
152 | };
153 |
154 | export { hexToRgb, hslToRgb, rgbToRgb, namedToRgb, toRgb };
155 |
--------------------------------------------------------------------------------
/src/utils/to-rgba.test.ts:
--------------------------------------------------------------------------------
1 | import { hexaToRgba, hslaToRgba, rgbaToRgba, toRgba } from "./to-rgba";
2 |
3 | describe("HexA to RGBA conversion", () => {
4 | it("correct rgba for black", () => {
5 | expect(hexaToRgba("#00000000")[0]).toBe(0);
6 | expect(hexaToRgba("#00000000")[1]).toBe(0);
7 | expect(hexaToRgba("#00000000")[2]).toBe(0);
8 | expect(hexaToRgba("#00000000")[3]).toBe(0);
9 |
10 | expect(hexaToRgba("00000000")[0]).toBe(0);
11 | expect(hexaToRgba("00000000")[1]).toBe(0);
12 | expect(hexaToRgba("00000000")[2]).toBe(0);
13 | expect(hexaToRgba("00000000")[3]).toBe(0);
14 |
15 | expect(hexaToRgba("#0000")[0]).toBe(0);
16 | expect(hexaToRgba("#0000")[1]).toBe(0);
17 | expect(hexaToRgba("#0000")[2]).toBe(0);
18 | expect(hexaToRgba("#0000")[3]).toBe(0);
19 |
20 | expect(hexaToRgba("0000")[0]).toBe(0);
21 | expect(hexaToRgba("0000")[1]).toBe(0);
22 | expect(hexaToRgba("0000")[2]).toBe(0);
23 | expect(hexaToRgba("0000")[3]).toBe(0);
24 | });
25 |
26 | it("correct rgba for white", () => {
27 | expect(hexaToRgba("#ffffff00")[0]).toBe(255);
28 | expect(hexaToRgba("#ffffff00")[1]).toBe(255);
29 | expect(hexaToRgba("#ffffff00")[2]).toBe(255);
30 | expect(hexaToRgba("#ffffff00")[3]).toBe(0);
31 |
32 | expect(hexaToRgba("ffffff00")[0]).toBe(255);
33 | expect(hexaToRgba("ffffff00")[1]).toBe(255);
34 | expect(hexaToRgba("ffffff00")[2]).toBe(255);
35 | expect(hexaToRgba("ffffff00")[3]).toBe(0);
36 |
37 | expect(hexaToRgba("#fff0")[0]).toBe(255);
38 | expect(hexaToRgba("#fff0")[1]).toBe(255);
39 | expect(hexaToRgba("#fff0")[2]).toBe(255);
40 | expect(hexaToRgba("#fff0")[3]).toBe(0);
41 |
42 | expect(hexaToRgba("fff0")[0]).toBe(255);
43 | expect(hexaToRgba("fff0")[1]).toBe(255);
44 | expect(hexaToRgba("fff0")[2]).toBe(255);
45 | expect(hexaToRgba("fff0")[3]).toBe(0);
46 | });
47 |
48 | it("correct rgba for hotpink", () => {
49 | expect(hexaToRgba("#ff69b400")[0]).toBe(255);
50 | expect(hexaToRgba("#ff69b400")[1]).toBe(105);
51 | expect(hexaToRgba("#ff69b400")[2]).toBe(180);
52 | expect(hexaToRgba("#ff69b400")[3]).toBe(0);
53 | });
54 | });
55 |
56 | describe("HSLa to RGBa conversion", () => {
57 | it("correct rgb for black", () => {
58 | expect(hslaToRgba("hsla(0, 0%, 0%, 1)")[0]).toBe(0);
59 | expect(hslaToRgba("hsla(0, 0%, 0%, 1)")[1]).toBe(0);
60 | expect(hslaToRgba("hsla(0, 0%, 0%, 1)")[2]).toBe(0);
61 | expect(hslaToRgba("hsla(0, 0%, 0%, 1)")[3]).toBe(1);
62 | });
63 |
64 | it("correct rgba for white", () => {
65 | expect(hslaToRgba("hsla(0, 0%, 100%, 1)")[0]).toBe(255);
66 | expect(hslaToRgba("hsla(0, 0%, 100%, 1)")[1]).toBe(255);
67 | expect(hslaToRgba("hsla(0, 0%, 100%, 1)")[2]).toBe(255);
68 | expect(hslaToRgba("hsla(0, 0%, 100%, 1)")[3]).toBe(1);
69 | });
70 | });
71 |
72 | describe("rgba to RGBA conversion", () => {
73 | it("correct rgb for black", () => {
74 | expect(rgbaToRgba("rgba(0, 0, 0, 1)")[0]).toBe(0);
75 | expect(rgbaToRgba("rgba(0, 0, 0, 1)")[1]).toBe(0);
76 | expect(rgbaToRgba("rgba(0, 0, 0, 1)")[2]).toBe(0);
77 | expect(rgbaToRgba("rgba(0, 0, 0, 1)")[3]).toBe(1);
78 | });
79 |
80 | it("correct rgba for white", () => {
81 | expect(rgbaToRgba("rgba(255, 255, 255, 1)")[0]).toBe(255);
82 | expect(rgbaToRgba("rgba(255, 255, 255, 1)")[1]).toBe(255);
83 | expect(rgbaToRgba("rgba(255, 255, 255, 1)")[2]).toBe(255);
84 | expect(rgbaToRgba("rgba(255, 255, 255, 1)")[3]).toBe(1);
85 | });
86 | });
87 |
88 | // integration
89 | describe("To RGB conversion", () => {
90 | it("correct rgba for black", () => {
91 | expect(toRgba("hsla(0, 0%, 0%, 1)")).toEqual([0, 0, 0, 1]);
92 | expect(toRgba("rgba(0, 0, 0, 1)")).toEqual([0, 0, 0, 1]);
93 | expect(toRgba("#000000ff")).toEqual([0, 0, 0, 1]);
94 | expect(toRgba("#00000000")).toEqual([0, 0, 0, 0]);
95 | });
96 |
97 | it("correct rgba for white", () => {
98 | expect(toRgba("hsla(0, 0%, 100%, 1)")).toEqual([255, 255, 255, 1]);
99 | expect(toRgba("rgba(255, 255, 255, 1)")).toEqual([255, 255, 255, 1]);
100 | expect(toRgba("#ffffffff")).toEqual([255, 255, 255, 1]);
101 | expect(toRgba("#ffffff00")).toEqual([255, 255, 255, 0]);
102 | });
103 |
104 | it("get undefined for colors without alpha channels", () => {
105 | expect(toRgba("hsl(0, 0%, 100%)")).toBeUndefined();
106 | expect(toRgba("rgb(255, 255, 255)")).toBeUndefined();
107 | expect(toRgba("white")).toBeUndefined();
108 | expect(toRgba("foo")).toBeUndefined();
109 | });
110 | });
111 |
--------------------------------------------------------------------------------
/src/utils/to-rgba.tsx:
--------------------------------------------------------------------------------
1 | import { typeOfColor } from "./type-of-color";
2 |
3 | // handles #000 or #000000
4 | // based on this function: https://css-tricks.com/converting-color-spaces-in-javascript/#article-header-id-3
5 | const hexaToRgba = (hex: string) => {
6 | let r: string | number = 0;
7 | let g: string | number = 0;
8 | let b: string | number = 0;
9 | let a: string | number = 1;
10 |
11 | if (hex.length === 4) {
12 | r = "0x" + hex[0] + hex[0];
13 | g = "0x" + hex[1] + hex[1];
14 | b = "0x" + hex[2] + hex[2];
15 | a = "0x" + hex[3] + hex[3];
16 | } else if (hex.length === 5) {
17 | r = "0x" + hex[1] + hex[1];
18 | g = "0x" + hex[2] + hex[2];
19 | b = "0x" + hex[3] + hex[3];
20 | a = "0x" + hex[4] + hex[4];
21 | } else if (hex.length === 8) {
22 | r = "0x" + hex[0] + hex[1];
23 | g = "0x" + hex[2] + hex[3];
24 | b = "0x" + hex[4] + hex[5];
25 | a = "0x" + hex[6] + hex[7];
26 | } else if (hex.length === 9) {
27 | r = "0x" + hex[1] + hex[2];
28 | g = "0x" + hex[3] + hex[4];
29 | b = "0x" + hex[5] + hex[6];
30 | a = "0x" + hex[7] + hex[8];
31 | }
32 | a = +((a as number) / 255).toFixed(3);
33 |
34 | return [+r, +g, +b, +a];
35 | };
36 |
37 | type Deg = number;
38 | type Rad = number;
39 | type Turn = number;
40 | type Hue = Deg | Rad | Turn;
41 |
42 | const stringToHue = (input: string): Hue => {
43 | const inputAsNum = Number(input.substr(0, input.length - 3));
44 |
45 | if (input.indexOf("deg") > -1) {
46 | return inputAsNum;
47 | } else if (input.indexOf("rad") > -1) {
48 | return Math.round(inputAsNum * (180 / Math.PI));
49 | } else if (input.indexOf("turn") > -1) {
50 | return Math.round(Number(input.substr(0, input.length - 4)) * 360);
51 | } else {
52 | return Number(input);
53 | }
54 | };
55 |
56 | // @TODO untangle this type
57 | const hslaToRgba = (hslaArg: any): number[] => {
58 | const sep: string = hslaArg.indexOf(",") > -1 ? "," : " ";
59 |
60 | const hsla: any = hslaArg
61 | .substr(5)
62 | .split(")")[0]
63 | .split(sep);
64 |
65 | // console.log(typeof hsla);
66 | // console.log(hsla);
67 |
68 | if (hsla.indexOf("/") > -1) hsla.splice(3, 1);
69 |
70 | let h: Hue = stringToHue(hsla[0]);
71 | let s = parseInt(hsla[1].substr(0, hsla[1].length - 1)) / 100;
72 | let l = parseInt(hsla[2].substr(0, hsla[2].length - 1)) / 100;
73 | let a = hsla[3];
74 |
75 | // Keep hue fraction of 360 if ending up over
76 | if (h >= 360) {
77 | h %= 360;
78 | }
79 |
80 | let c = (1 - Math.abs(2 * l - 1)) * s,
81 | x = c * (1 - Math.abs(((h / 60) % 2) - 1)),
82 | m = l - c / 2,
83 | r = 0,
84 | g = 0,
85 | b = 0;
86 | if (0 <= h && h < 60) {
87 | r = c;
88 | g = x;
89 | b = 0;
90 | } else if (60 <= h && h < 120) {
91 | r = x;
92 | g = c;
93 | b = 0;
94 | } else if (120 <= h && h < 180) {
95 | r = 0;
96 | g = c;
97 | b = x;
98 | } else if (180 <= h && h < 240) {
99 | r = 0;
100 | g = x;
101 | b = c;
102 | } else if (240 <= h && h < 300) {
103 | r = x;
104 | g = 0;
105 | b = c;
106 | } else if (300 <= h && h < 360) {
107 | r = c;
108 | g = 0;
109 | b = x;
110 | }
111 | r = Math.round((r + m) * 255);
112 | g = Math.round((g + m) * 255);
113 | b = Math.round((b + m) * 255);
114 |
115 | return [+r, +g, +b, +a];
116 | };
117 |
118 | const rgbaToRgba = (rgba: any) => {
119 | const sep = rgba.indexOf(",") > -1 ? "," : " ";
120 |
121 | rgba = rgba
122 | .substr(5)
123 | .split(")")[0]
124 | .split(sep);
125 |
126 | const r = rgba[0];
127 | const g = rgba[1];
128 | const b = rgba[2];
129 | const a = rgba[3];
130 |
131 | return [+r, +g, +b, +a];
132 | };
133 |
134 | const toRgba = (color: string) => {
135 | switch (true) {
136 | case typeOfColor(color) === "hex4":
137 | case typeOfColor(color) === "hex8":
138 | return hexaToRgba(color);
139 |
140 | case typeOfColor(color) === "rgba":
141 | return rgbaToRgba(color);
142 |
143 | case typeOfColor(color) === "hsla":
144 | return hslaToRgba(color);
145 |
146 | // case typeOfColor(color) === "named":
147 | // return namedToRgba(color);
148 |
149 | default:
150 | return undefined;
151 | }
152 | };
153 |
154 | export { hexaToRgba, hslaToRgba, rgbaToRgba, toRgba };
155 |
--------------------------------------------------------------------------------
/src/utils/type-of-color.test.ts:
--------------------------------------------------------------------------------
1 | import { typeOfColor, isValidColor } from "./type-of-color";
2 |
3 | describe("Type Of Color", () => {
4 | it("returns none for a bad value", () => {
5 | expect(typeOfColor("foo")).toBe("none");
6 | expect(typeOfColor("#foo")).toBe("none");
7 | expect(typeOfColor("#fooo")).toBe("none");
8 | expect(typeOfColor("foobar")).toBe("none");
9 | expect(typeOfColor("#foobar")).toBe("none");
10 | });
11 |
12 | it("returns named for a named color", () => {
13 | expect(typeOfColor("RebeccaPurple")).toBe("named");
14 | expect(typeOfColor("rebeccapurple")).toBe("named");
15 | expect(typeOfColor("REBECCAPURPLE")).toBe("named");
16 | });
17 |
18 | it("returns hex6", () => {
19 | expect(typeOfColor("#ffffff")).toBe("hex6");
20 | expect(typeOfColor("ffffff")).toBe("hex6");
21 | });
22 |
23 | it("returns hex3", () => {
24 | expect(typeOfColor("#fff")).toBe("hex3");
25 | expect(typeOfColor("fff")).toBe("hex3");
26 | });
27 |
28 | it("returns hex4", () => {
29 | expect(typeOfColor("#fff0")).toBe("hex4");
30 | expect(typeOfColor("fff0")).toBe("hex4");
31 | });
32 |
33 | it("returns hex8", () => {
34 | expect(typeOfColor("#ffffff0A")).toBe("hex8");
35 | expect(typeOfColor("ffffff0A")).toBe("hex8");
36 | });
37 |
38 | it("returns rgba", () => {
39 | expect(typeOfColor("rgba(255, 255, 255, 1)")).toBe("rgba");
40 | });
41 |
42 | it("returns rgb", () => {
43 | expect(typeOfColor("rgb(255, 255, 255)")).toBe("rgb");
44 | });
45 |
46 | it("returns hsla", () => {
47 | expect(typeOfColor("hsla(0, 0%, 100, 1)")).toBe("hsla");
48 | });
49 |
50 | it("returns hsl", () => {
51 | expect(typeOfColor("hsl(0, 0%, 100)")).toBe("hsl");
52 | });
53 | });
54 |
55 | describe("Is Valid Color", () => {
56 | it("returns false for a bad value", () => {
57 | expect(isValidColor("foo")).toBe(false);
58 | expect(isValidColor("#foo")).toBe(false);
59 | expect(isValidColor("#fooo")).toBe(false);
60 | expect(isValidColor("#foobar")).toBe(false);
61 | });
62 |
63 | it("returns true for a good value", () => {
64 | expect(isValidColor("fff")).toBe(true);
65 | expect(isValidColor("#fff")).toBe(true);
66 | expect(isValidColor("#ffff")).toBe(true);
67 | expect(isValidColor("rgb(255, 255, 255)")).toBe(true);
68 | expect(isValidColor("hsla(0, 0%, 100, 1)")).toBe(true);
69 | });
70 |
71 |
72 | });
73 |
--------------------------------------------------------------------------------
/src/utils/type-of-color.tsx:
--------------------------------------------------------------------------------
1 | import { namedColors } from "./named-colors";
2 |
3 | // taking the named colors and converting them to lowercase
4 | // for comparison
5 | const lowerCaseNamedColors = namedColors.map(c => c.toLowerCase());
6 |
7 | const typeOfColor = (color: string): string => {
8 | switch (true) {
9 | // https://stackoverflow.com/a/8027444/1173898
10 | case /^(#)?[0-9A-F]{3}$/i.test(color):
11 | return "hex3";
12 |
13 | case /^(#)?[0-9A-F]{6}$/i.test(color):
14 | return "hex6";
15 |
16 | case /^(#)?[0-9A-F]{4}$/i.test(color):
17 | return "hex4";
18 |
19 | case /^(#)?[0-9A-F]{8}$/i.test(color):
20 | return "hex8";
21 |
22 | case color.indexOf("rgba") === 0 && color.indexOf(")") !== -1:
23 | return "rgba";
24 |
25 | case color.indexOf("rgb") === 0 && color.indexOf(")") !== -1:
26 | return "rgb";
27 |
28 | case color.indexOf("hsla") === 0 && color.indexOf(")") !== -1:
29 | return "hsla";
30 |
31 | case color.indexOf("hsl") === 0 && color.indexOf(")") !== -1:
32 | return "hsl";
33 |
34 | // converting user input to lowercase so the input
35 | // can be "rebeccapurple" or "RebeccaPurple"
36 | case lowerCaseNamedColors.includes(color.toLowerCase()):
37 | return "named";
38 |
39 | default:
40 | return "none";
41 | }
42 | };
43 |
44 | const isValidColor = (color: string): boolean => {
45 | switch (true) {
46 | // https://stackoverflow.com/a/8027444/1173898
47 | case /^(#)?[0-9A-F]{3}$/i.test(color):
48 | case /^(#)?[0-9A-F]{6}$/i.test(color):
49 | case /^(#)?[0-9A-F]{4}$/i.test(color):
50 | case /^(#)?[0-9A-F]{8}$/i.test(color):
51 | case color.indexOf("rgba") === 0 && color.indexOf(")") !== -1:
52 | case color.indexOf("rgb") === 0 && color.indexOf(")") !== -1:
53 | case color.indexOf("hsla") === 0 && color.indexOf(")") !== -1:
54 | case color.indexOf("hsl") === 0 && color.indexOf(")") !== -1:
55 | case lowerCaseNamedColors.includes(color.toLowerCase()):
56 | return true;
57 | default:
58 | return false;
59 | }
60 | };
61 |
62 | export { typeOfColor, isValidColor };
63 |
--------------------------------------------------------------------------------
/src/utils/useQueryString.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from "react";
2 | import { getQueryStringValue, setQueryStringValue} from './queryString';
3 |
4 | export const useQueryString = (key:string, initialValue?: string | boolean) => {
5 | const [value, setValue] = useState(getQueryStringValue(key) || initialValue);
6 |
7 | const onSetValue = useCallback(
8 | newValue => {
9 | setValue(newValue);
10 | setQueryStringValue(key, newValue)
11 | },
12 | [key],
13 | );
14 |
15 | return [value, onSetValue] as const;
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "noFallthroughCasesInSwitch": true,
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------