├── .gitattributes ├── .github └── workflows │ ├── pr_check.yml │ └── release.yml ├── .gitignore ├── .releaserc ├── CHANGELOG.md ├── README.md ├── bsconfig.json ├── package-lock.json ├── package.json ├── postcss.config.js ├── public └── index.html ├── src ├── App.res ├── Clipboard.res ├── Hooks.res ├── Index.res ├── WCAG.res ├── components │ ├── ColorField.res │ ├── PasteBoard.res │ └── Score.res ├── index.css └── index.js ├── tailwind.config.js ├── test └── WCAG__test.res └── webpack.config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | *.res linguist-language=reason 2 | -------------------------------------------------------------------------------- /.github/workflows/pr_check.yml: -------------------------------------------------------------------------------- 1 | name: Run tests on PR 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | test: 10 | name: Test on node ${{ matrix.node_version }} and ${{ matrix.os }} 11 | 12 | runs-on: ${{ matrix.os }} 13 | 14 | strategy: 15 | matrix: 16 | node-version: [10.x, 12.x] 17 | os: [ubuntu-latest, windows-latest, macOS-latest] 18 | 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - name: Run tests and linting 27 | run: | 28 | npm ci 29 | npm run build 30 | npm test 31 | env: 32 | CI: true 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - uses: actions/setup-node@v1 16 | 17 | - name: Run tests and linting 18 | run: | 19 | npm install 20 | npm run build 21 | npm test 22 | env: 23 | CI: true 24 | 25 | - name: Create release using semantic-release 26 | run: npx semantic-release 27 | env: 28 | # GITHUB_TOKEN is added automatically by GitHub 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Build folders 5 | /lib 6 | *.bs.js 7 | 8 | # Misc 9 | .DS_Store 10 | .merlin 11 | .bsb.lock 12 | npm-debug.log 13 | 14 | dist 15 | 16 | .vercel 17 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@semantic-release/commit-analyzer", 4 | "@semantic-release/release-notes-generator", 5 | "@semantic-release/changelog", 6 | ["@semantic-release/npm", { 7 | "npmPublish": false 8 | }], 9 | "@semantic-release/git" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [1.4.0](https://github.com/believer/color/compare/v1.3.0...v1.4.0) (2021-02-19) 2 | 3 | 4 | ### Features 5 | 6 | * add fg and bg url params for colors ([8c0cfa4](https://github.com/believer/color/commit/8c0cfa4c8657837de288a93d2dfbb0d477fd1be7)) 7 | 8 | # [1.3.0](https://github.com/believer/color/compare/v1.2.3...v1.3.0) (2020-08-31) 9 | 10 | 11 | ### Features 12 | 13 | * add score explanation and paste hint ([cabfb9a](https://github.com/believer/color/commit/cabfb9a24a9781d710197de5ba2755094dc246fe)) 14 | 15 | ## [1.2.3](https://github.com/believer/color/compare/v1.2.2...v1.2.3) (2020-08-31) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * handle pasted hex colors without hash ([eafc738](https://github.com/believer/color/commit/eafc738c9976f981cecc43e73f75f0011412d524)) 21 | 22 | ## [1.2.2](https://github.com/believer/color/compare/v1.2.1...v1.2.2) (2020-08-30) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * indicate when colors are equal fixes [#5](https://github.com/believer/color/issues/5) ([41a06e3](https://github.com/believer/color/commit/41a06e336e73fc0ec6fe98a7bf4ce250c99d7f71)) 28 | * support for hex colors without hash fixes [#4](https://github.com/believer/color/issues/4) ([974d513](https://github.com/believer/color/commit/974d5132978284b9a76723bad80e37817e66247c)) 29 | 30 | ## [1.2.1](https://github.com/believer/color/compare/v1.2.0...v1.2.1) (2020-08-29) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * handle hsl without percent and hex without hash ([1087405](https://github.com/believer/color/commit/1087405ca7f677190685b33420b1c45b23ba58c3)) 36 | 37 | # [1.2.0](https://github.com/believer/color/compare/v1.1.0...v1.2.0) (2020-08-29) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * increase score color contrast ([dc11a94](https://github.com/believer/color/commit/dc11a943b7057f797fc57b53b254bf41bdb2d69c)) 43 | 44 | 45 | ### Features 46 | 47 | * add color picker fixes [#3](https://github.com/believer/color/issues/3) ([3a39e7d](https://github.com/believer/color/commit/3a39e7d1feda9c440feba13d5b652cdf61efd54f)) 48 | 49 | # [1.1.0](https://github.com/believer/color/compare/v1.0.0...v1.1.0) (2020-08-28) 50 | 51 | 52 | ### Features 53 | 54 | * add paste color string support fixes [#2](https://github.com/believer/color/issues/2) ([6641db7](https://github.com/believer/color/commit/6641db7a575b0dc38f99b9dd0a76c4a2df6cd60e)) 55 | 56 | # 1.0.0 (2020-08-28) 57 | 58 | 59 | ### Bug Fixes 60 | 61 | * invalid hex colors ([59c5beb](https://github.com/believer/color/commit/59c5beba27e80cb5fd06f12f18e6a46ffbb5b207)) 62 | 63 | 64 | ### Features 65 | 66 | * initial commit ([8460e21](https://github.com/believer/color/commit/8460e21284f5194ee0a98608f0d9b2dc345de7ae)) 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Color 2 | 3 | [![](https://github.com/believer/color/workflows/Release/badge.svg)](https://github.com/believer/color/actions?workflow=Release) 4 | 5 | Calculate WCAG ratio and score between two colors using [wcag-color](https://github.com/believer/wcag-color/). 6 | Try it at [https://color.willcodefor.beer](https://color.willcodefor.beer) 7 | 8 | ## Run Project 9 | 10 | ```sh 11 | npm install 12 | npm start 13 | # in another tab 14 | npm run server 15 | ``` 16 | 17 | When both processes are running, open a browser at http://localhost:3000 18 | 19 | ## Build for Production 20 | 21 | ```sh 22 | npm run clean 23 | npm run build 24 | npm run webpack:production 25 | ``` 26 | 27 | This will replace the development artifact `build/Index.js` for an optimized 28 | version as well as copy `public/index.html` into `build/`. You can then deploy the 29 | contents of the `build` directory (`index.html` and `Index.js`). 30 | 31 | If you make use of routing (via `ReasonReact.Router` or similar logic) ensure 32 | that server-side routing handles your routes or that 404's are directed back to 33 | `index.html` (which is how the dev server is set up). 34 | 35 | **To enable dead code elimination**, change `bsconfig.json`'s `package-specs` 36 | `module` from `"commonjs"` to `"es6"`. Then re-run the above 2 commands. This 37 | will allow Webpack to remove unused code. 38 | ' 39 | 40 | ## Build to Now 41 | 42 | This project includes building straight to [Now](https://zeit.co/) after Travis has validated 43 | tests and created a release. There are some steps that need to be taken to enable the setup. 44 | 45 | 1. Get a token from your [Now dashboard](https://zeit.co/account/tokens) 46 | 1. Set the token as `NOW_TOKEN` in Travis 47 | 1. Uncomment the Now build steps in `.travis.yml` 48 | 1. Add `now-build` to `package.json` scripts. Now runs this script during it build process: 49 | 50 | ``` 51 | "now-build": "npm run build && npm run webpack:production" 52 | ``` 53 | -------------------------------------------------------------------------------- /bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "color", 3 | "reason": { 4 | "react-jsx": 3 5 | }, 6 | "sources": [ 7 | { 8 | "dir": "src", 9 | "subdirs": true 10 | }, 11 | { "dir": "test", "subdirs": true, "type": "dev" } 12 | ], 13 | "package-specs": [ 14 | { 15 | "module": "commonjs", 16 | "in-source": true 17 | } 18 | ], 19 | "suffix": ".bs.js", 20 | "namespace": true, 21 | "bs-dependencies": ["reason-react", "bs-webapi", "reason-test-framework"], 22 | "refmt": 3, 23 | "warnings": { 24 | "number": "+A-9-40-42-48", 25 | "error": "+A-3-40-42-44-102" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "color", 3 | "version": "1.4.0", 4 | "scripts": { 5 | "build": "bsb -make-world", 6 | "start": "cross-env BS_WATCH_CLEAR=true bsb -make-world -w", 7 | "clean": "bsb -clean-world", 8 | "test": "is-ci \"test:ci\" \"test:watch\"", 9 | "test:ci": "jest", 10 | "test:watch": "jest --watch", 11 | "webpack": "webpack -w", 12 | "webpack:production": "NODE_ENV=production webpack", 13 | "server": "webpack serve", 14 | "vercel-build": "rimraf dist && npm run build && npm run webpack:production" 15 | }, 16 | "keywords": [ 17 | "BuckleScript" 18 | ], 19 | "author": "", 20 | "license": "MIT", 21 | "dependencies": { 22 | "bs-webapi": "0.19.1", 23 | "react": "17.0.1", 24 | "react-dom": "17.0.1", 25 | "reason-react": "0.9.1" 26 | }, 27 | "devDependencies": { 28 | "@semantic-release/changelog": "5.0.1", 29 | "@semantic-release/git": "9.0.0", 30 | "autoprefixer": "10.2.4", 31 | "bs-platform": "9.0.1", 32 | "cross-env": "7.0.3", 33 | "css-loader": "5.0.2", 34 | "html-webpack-plugin": "5.1.0", 35 | "is-ci-cli": "2.2.0", 36 | "jest": "26.6.3", 37 | "postcss-loader": "5.0.0", 38 | "reason-test-framework": "0.3.2", 39 | "rimraf": "3.0.2", 40 | "style-loader": "2.0.0", 41 | "tailwindcss": "2.0.3", 42 | "webpack": "5.23.0", 43 | "webpack-cli": "4.5.0", 44 | "webpack-dev-server": "3.11.2" 45 | }, 46 | "jest": { 47 | "moduleDirectories": [ 48 | "node_modules" 49 | ], 50 | "roots": [ 51 | "test" 52 | ], 53 | "testMatch": [ 54 | "**/*__test.bs.js" 55 | ], 56 | "transformIgnorePatterns": [ 57 | "node_modules/(?!(bs-platform)/)" 58 | ] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require("tailwindcss"), require("autoprefixer")], 3 | }; 4 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | color 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/App.res: -------------------------------------------------------------------------------- 1 | type state = {colors: (string, string)} 2 | 3 | type action = 4 | | SetForegroundColor(string) 5 | | SetBackgroundColor(string) 6 | 7 | @react.component 8 | let make = () => { 9 | let colors = Hooks.useColorFromUrl() 10 | 11 | let (state, dispatch) = React.useReducer((state, action) => { 12 | let (foregroundColor, backgroundColor) = state.colors 13 | 14 | switch action { 15 | | SetBackgroundColor(color) => {colors: (foregroundColor, color)} 16 | | SetForegroundColor(color) => {colors: (color, backgroundColor)} 17 | } 18 | }, {colors: colors}) 19 | let (foregroundColor, backgroundColor) = state.colors 20 | 21 | let handleChange = event => { 22 | (event->ReactEvent.Form.target)["value"]->Js.String2.trim 23 | } 24 | 25 |
26 |
29 |
32 | {switch WCAG.Score.make(foregroundColor, backgroundColor)->WCAG.Score.toString { 33 | | Some("Equal") => 34 |
39 | {"Colors are the same"->React.string} 40 |
41 | | Some(score) => score->React.string 42 | | None => React.null 43 | }} 44 |
45 |
51 | 52 |
53 | dispatch(SetForegroundColor(color))} 55 | setBackgroundColor={color => dispatch(SetBackgroundColor(color))} 56 | /> 57 |
58 |
59 |
60 | { 64 | let value = handleChange(e) 65 | dispatch(SetForegroundColor(value)) 66 | }} 67 | value=foregroundColor 68 | /> 69 | { 73 | let value = handleChange(e) 74 | dispatch(SetBackgroundColor(value)) 75 | }} 76 | value=backgroundColor 77 | /> 78 |
79 |
80 | {"Psst.. You can also paste colors on the page"->React.string} 81 |
82 |
83 |
84 | } 85 | -------------------------------------------------------------------------------- /src/Clipboard.res: -------------------------------------------------------------------------------- 1 | open Webapi 2 | 3 | module Data = { 4 | external toClipboardEvent: Dom.Event.t => Dom.ClipboardEvent.t = "%identity" 5 | @send external make: ('a, string) => string = "getData" 6 | } 7 | 8 | let use = () => { 9 | let (clipboardData, setClipboardData) = React.useState(() => None) 10 | 11 | React.useEffect0(() => { 12 | let listener = event => { 13 | let data = event->Data.toClipboardEvent->Dom.ClipboardEvent.clipboardData->Data.make("text") 14 | 15 | setClipboardData(_ => Some(data)) 16 | } 17 | 18 | Dom.window |> Dom.Window.addEventListener("paste", listener) 19 | 20 | Some(() => {Dom.window |> Dom.Window.removeEventListener("paste", listener)}) 21 | }) 22 | 23 | (clipboardData, setClipboardData) 24 | } 25 | -------------------------------------------------------------------------------- /src/Hooks.res: -------------------------------------------------------------------------------- 1 | module Url = { 2 | type t 3 | 4 | @new external params: string => t = "URLSearchParams" 5 | @send external get: (t, string) => Js.Nullable.t = "get" 6 | } 7 | 8 | let useColorFromUrl = () => { 9 | let url = ReasonReactRouter.useUrl() 10 | let defaultColors = ("#ffffff", "#000000") 11 | 12 | let colors = switch url.search { 13 | | "" => defaultColors 14 | | search => 15 | switch ( 16 | Url.params(search)->Url.get("fg")->Js.Nullable.toOption, 17 | Url.params(search)->Url.get("bg")->Js.Nullable.toOption, 18 | ) { 19 | | (Some(fg), Some(bg)) => 20 | switch (fg, bg) { 21 | | ("", "") => defaultColors 22 | | (fg, bg) => (fg, bg) 23 | } 24 | 25 | | (Some(fg), None) => 26 | switch fg { 27 | | "" => defaultColors 28 | | fg => (fg, "#000000") 29 | } 30 | 31 | | (None, Some(bg)) => 32 | switch bg { 33 | | "" => defaultColors 34 | | bg => ("#ffffff", bg) 35 | } 36 | 37 | | (None, None) => defaultColors 38 | } 39 | } 40 | 41 | colors 42 | } 43 | -------------------------------------------------------------------------------- /src/Index.res: -------------------------------------------------------------------------------- 1 | ReactDOMRe.renderToElementWithId(, "root"); 2 | 3 | -------------------------------------------------------------------------------- /src/WCAG.res: -------------------------------------------------------------------------------- 1 | module Utils = { 2 | let removeHash = str => str |> Js.String.replace("#", "") 3 | } 4 | 5 | module Luminance = { 6 | let toSRGB = color => color /. 255.0 7 | let toRGB = color => 8 | color <= 0.03928 ? color /. 12.92 : Js.Math.pow_float(~base=(color +. 0.055) /. 1.055, ~exp=2.4) 9 | 10 | let relative = rgb => 11 | switch rgb { 12 | | [r, g, b] => r *. 0.2126 +. g *. 0.7152 +. b *. 0.0722 13 | | _ => 0.0 14 | } 15 | 16 | /* 17 | * https://www.w3.org/WAI/GL/wiki/Relative_luminance 18 | */ 19 | let convert = color => { 20 | open Js.Array 21 | 22 | color |> map(toSRGB) |> map(toRGB) |> relative 23 | } 24 | } 25 | 26 | module HSL = { 27 | let hueToRgb = (p, q, t) => { 28 | switch t { 29 | | x when x < 1.0 /. 6.0 => p +. (q -. p) *. 6.0 *. x 30 | | x when x < 0.5 => q 31 | | x when x < 2.0 /. 3.0 => p +. (q -. p) *. 6.0 *. (2.0 /. 3.0 -. x) 32 | | _ => p 33 | } 34 | } 35 | 36 | let createRgbFromHsl = (h, s, l) => { 37 | /* Get hue by rotation (360deg) */ 38 | let hue = h /. 3.6 39 | let tempR = hue +. 1.0 /. 3.0 40 | let tempB = hue -. 1.0 /. 3.0 41 | 42 | let q = switch l { 43 | | l when l < 0.5 => l *. (1.0 +. s) 44 | | _ => l +. s -. l *. s 45 | } 46 | let p = 2.0 *. l -. q 47 | let rgb = hueToRgb(p, q) 48 | 49 | let b = switch tempB { 50 | | x when x < 0. => 0. 51 | | x => x 52 | } 53 | 54 | [rgb(tempR), rgb(hue), rgb(b)] 55 | } 56 | 57 | /* 58 | * http://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/ 59 | * https://gist.github.com/mjackson/5311256 60 | */ 61 | let convert = hsl => { 62 | hsl 63 | |> Js.Array.map(x => x /. 100.0) 64 | |> (hsl => 65 | switch hsl { 66 | | [_, 0.0, l] => [l, l, l] 67 | | [h, s, l] when h === 3.6 => createRgbFromHsl(0., s, l) 68 | | [h, s, l] => createRgbFromHsl(h, s, l) 69 | | _ => [] 70 | }) 71 | |> Js.Array.map(x => x *. 255.0) 72 | } 73 | } 74 | 75 | module HEX = { 76 | let defaultArray = str => str->Belt.Option.getWithDefault([]) 77 | 78 | let hexParts = t => { 79 | switch t->Js.String.length { 80 | | 3 => Js.String2.match_(t, %re("/.{1}/g")) |> defaultArray |> Js.Array.map(x => x ++ x) 81 | | _ => Js.String2.match_(t, %re("/.{2}/g")) |> defaultArray 82 | } 83 | } 84 | 85 | let convert = hex => { 86 | hex |> hexParts |> Js.Array.map(x => "0x" ++ x |> float_of_string) 87 | } 88 | } 89 | 90 | module Validate = { 91 | let make = input => { 92 | let valid = %re( 93 | `/(^#?\\w{3}$)|(^#?\\w{6}$)|(^rgb\\(\\d{1,3},\\s?\\d{1,3},\\s?\\d{1,3}\\)$)|(^hsl\\(\\d{1,3},\\s?\\d{1,3}%?,\\s?\\d{1,3}%?\\)$)/` 94 | ) 95 | 96 | Js.Re.test_(valid, input) 97 | } 98 | 99 | let parse = input => { 100 | let hex = %re(`/(^#?\\w{3}$)|(^#?\\w{6}$)/`) 101 | 102 | switch input { 103 | | input when hex->Js.Re.test_(input) => `#${Utils.removeHash(input)}` 104 | | input => input 105 | } 106 | } 107 | } 108 | 109 | module Ratio = { 110 | type t = 111 | | HSL 112 | | RGB 113 | | HEX 114 | 115 | let typeOfColor = color => 116 | switch color |> Js.String.substring(~from=0, ~to_=3) { 117 | | "rgb" => RGB 118 | | "hsl" => HSL 119 | | _ => HEX 120 | } 121 | 122 | let parseNumbers = rgb => { 123 | switch rgb->Js.String2.match_(%re("/\\d+/g")) { 124 | | Some(colors) => colors |> Js.Array.map(x => x->float_of_string) 125 | | None => [] 126 | } 127 | } 128 | 129 | let parseColor = color => 130 | switch color |> typeOfColor { 131 | | HEX => color |> HEX.convert 132 | | HSL => color |> parseNumbers |> HSL.convert 133 | | RGB => color |> parseNumbers 134 | } 135 | |> Luminance.convert 136 | |> (v => v +. 0.05) 137 | 138 | let make = (foreground, background) => { 139 | switch (Validate.make(foreground), Validate.make(background)) { 140 | | (true, true) => 141 | switch (foreground |> Utils.removeHash, background |> Utils.removeHash) { 142 | | (fg, bg) when fg === bg => Some(1.0) 143 | | (fg, bg) => 144 | Some( 145 | switch (fg |> parseColor, bg |> parseColor) { 146 | | (f, b) when f > b => f /. b 147 | | (f, b) => b /. f 148 | } 149 | |> Js.Float.toFixedWithPrecision(~digits=2) 150 | |> Js.Float.fromString, 151 | ) 152 | } 153 | | _ => None 154 | } 155 | } 156 | } 157 | 158 | module Score = { 159 | type t = 160 | | AAA 161 | | AA 162 | | AALarge 163 | | Fail 164 | | Equal 165 | | Invalid 166 | 167 | let calculateFromRatio = ratio => 168 | switch ratio { 169 | | Some(r) when r >= 7.0 => AAA 170 | | Some(r) when r >= 4.5 => AA 171 | | Some(r) when r >= 3.0 => AALarge 172 | | Some(r) when r == 1.0 => Equal 173 | | Some(_) => Fail 174 | | None => Invalid 175 | } 176 | 177 | let toString = value => 178 | switch value { 179 | | AAA => Some("AAA") 180 | | AA => Some("AA") 181 | | AALarge => Some("AA Large") 182 | | Fail => Some("Fail") 183 | | Equal => Some("Equal") 184 | | Invalid => None 185 | } 186 | 187 | let make = (foreground, background) => Ratio.make(foreground, background) |> calculateFromRatio 188 | } 189 | 190 | module Best = { 191 | let make = (first, second, background) => { 192 | let firstRatio = Ratio.make(first, background) 193 | let secondRatio = Ratio.make(second, background) 194 | 195 | switch (firstRatio, secondRatio) { 196 | | (Some(f), Some(s)) when f > s => first 197 | | (Some(_), None) => first 198 | | _ => second 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/components/ColorField.res: -------------------------------------------------------------------------------- 1 | @react.component 2 | let make = (~id, ~value, ~label, ~onChange) => { 3 |
4 | 7 |
8 | 17 | 25 |
26 |
27 | } 28 | -------------------------------------------------------------------------------- /src/components/PasteBoard.res: -------------------------------------------------------------------------------- 1 | @react.component 2 | let make = (~setForegroundColor, ~setBackgroundColor) => { 3 | let (clipboardData, setClipboardData) = Clipboard.use() 4 | let (displayPasteBoard, setDisplayPasteBoard) = React.useState(() => false) 5 | 6 | React.useEffect1(() => { 7 | switch clipboardData { 8 | | Some(data) => WCAG.Validate.make(data) ? setDisplayPasteBoard(_ => true) : () 9 | | None => () 10 | } 11 | 12 | None 13 | }, [clipboardData]) 14 | 15 | let setPasteAsForegroundColor = _ => { 16 | switch clipboardData { 17 | | Some(data) => 18 | setForegroundColor(data) 19 | setClipboardData(_ => None) 20 | setDisplayPasteBoard(_ => false) 21 | | None => () 22 | } 23 | } 24 | 25 | let setPasteAsBackgroundColor = _ => { 26 | switch clipboardData { 27 | | Some(data) => 28 | setBackgroundColor(data) 29 | setClipboardData(_ => None) 30 | setDisplayPasteBoard(_ => false) 31 | | None => () 32 | } 33 | } 34 | 35 | switch displayPasteBoard { 36 | | false => React.null 37 | | true => 38 |
40 |
41 | {switch clipboardData { 42 | | Some(color) => 43 |
44 |
48 | {React.string("You pasted the color: ")} 49 | {color->WCAG.Validate.parse->React.string} 50 |
51 | | None => React.null 52 | }} 53 |
54 | 59 | 64 |
65 |
66 |
67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/components/Score.res: -------------------------------------------------------------------------------- 1 | @react.component 2 | let make = (~foregroundColor, ~backgroundColor) => { 3 | let score = WCAG.Score.make(foregroundColor, backgroundColor) 4 | 5 | <> 6 |
7 | {switch score { 8 | | AAA | AA | AALarge => 9 | 10 | 15 | 21 | 22 | 23 | | Equal | Fail | Invalid => React.null 24 | }} 25 | {switch WCAG.Ratio.make(foregroundColor, backgroundColor) { 26 | | Some(r) => {React.string("Score: ")} {React.float(r)} 27 | | None => React.null 28 | }} 29 |
30 |
31 | {switch score { 32 | | AAA => "AAA (> 7) is an enhanced contrast ratio. This is valuable for texts that will be read for a longer period of time." 33 | | AA => "AA (> 4.5) is what you should aim for with text sizes below 18px" 34 | | AALarge => "AA Large (> 3) is the least amount of contrast for font size 18px and larger" 35 | | Fail | Equal | Invalid => "Your text has a contrast ratio of less than 3.0" 36 | }->React.string} 37 |
38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body { 7 | @apply font-normal m-0 p-0; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | require("./index.css"); 2 | require("./Index.bs"); 3 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ["./src/**/*.bs.js"], 3 | theme: { 4 | extend: { 5 | colors: { 6 | primary: "#1e2031", 7 | }, 8 | }, 9 | }, 10 | variants: {}, 11 | plugins: [], 12 | future: { 13 | removeDeprecatedGapUtilities: true, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /test/WCAG__test.res: -------------------------------------------------------------------------------- 1 | open TestFramework 2 | 3 | describe("Best", ({test}) => { 4 | test("finds the best foreground color for a given background", ({expect}) => { 5 | list{ 6 | ("#ffffff", "#000000", "#ffffff", "#000000"), 7 | ("#ffffff", "#000000", "#000000", "#ffffff"), 8 | ("rgb(255,255,255)", "#000000", "#979798", "#000000"), 9 | ("rgb(255,255,255)", "#000000", "#fd8b56", "#000000"), 10 | ("hsl(0,0,100%)", "#000000", "#3e74b1", "hsl(0,0,100%)"), 11 | }->Belt.List.forEach(((one, two, bg, expected)) => { 12 | expect.string(WCAG.Best.make(one, two, bg)).toEqual(expected) 13 | }) 14 | }) 15 | }) 16 | 17 | describe("Ratio", ({test}) => { 18 | test("color ratios", ({expect}) => { 19 | list{ 20 | ("#ffffff", "#ffffff", 1.0), 21 | ("ffffff", "ffffff", 1.0), 22 | ("rgb(255, 255, 255)", "rgb(255, 255, 255)", 1.0), 23 | ("rgb(255,255,255)", "rgb(255, 255, 255)", 1.0), 24 | ("hsl(255, 30%, 40%)", "hsl(255, 30%, 40%)", 1.0), 25 | ("hsl(255,30,40)", "hsl(255, 30%, 40%)", 1.0), 26 | ("#ffffff", "rgb(255, 255, 255)", 1.0), 27 | ("#ffffff", "#000000", 21.0), 28 | ("#ffffff", "#777777", 4.48), 29 | ("#0088FF", "#C611AB", 1.47), 30 | ("ffffff", "777777", 4.48), 31 | ("fff", "777", 4.48), 32 | ("08f", "fff", 3.52), 33 | ("rgb(255,255,255)", "#777777", 4.48), 34 | ("rgb(255,255,255)", "rgb(77,77,77)", 8.45), 35 | ("hsl(0, 0%, 20%)", "#ffffff", 12.63), 36 | ("hsl(210, 30%, 48%)", "#ffffff", 4.47), 37 | ("hsl(210, 30%, 68%)", "#ffffff", 2.31), 38 | ("hsl(0, 0%, 20%)", "hsl(0, 0%, 100%)", 12.63), 39 | ("hsl(0, 100%, 40%)", "#fff", 5.89), 40 | ("hsl(360, 100%, 40%)", "#fff", 5.89), 41 | }->Belt.List.forEach(((fg, bg, expected)) => { 42 | expect.value(WCAG.Ratio.make(fg, bg)).toEqual(Some(expected)) 43 | }) 44 | }) 45 | 46 | test("validates colors", ({expect}) => { 47 | expect.value(WCAG.Ratio.make("#ffff", "#000000")).toEqual(None) 48 | }) 49 | }) 50 | 51 | describe("Score", ({test}) => { 52 | test("#calculateFromRatio", ({expect}) => { 53 | list{ 54 | (Some(7.1), WCAG.Score.AAA), 55 | (Some(4.6), AA), 56 | (Some(3.9), AALarge), 57 | (Some(2.9), Fail), 58 | }->Belt.List.forEach(((ratio, score)) => { 59 | expect.value(WCAG.Score.calculateFromRatio(ratio)).toEqual(score) 60 | }) 61 | }) 62 | 63 | test("#make", ({expect}) => 64 | list{ 65 | ("#ffffff", "#000000", WCAG.Score.AAA), 66 | ("#ffffff", "#666666", AA), 67 | ("#ffffff", "#888888", AALarge), 68 | ("#ffffff", "#cccccc", Fail), 69 | }->Belt.List.forEach(((foreground, background, score)) => { 70 | expect.value(WCAG.Score.make(foreground, background)).toEqual(score) 71 | }) 72 | ) 73 | }) 74 | 75 | describe("Validate", ({test}) => { 76 | test("parses hex colors", ({expect}) => { 77 | expect.string(WCAG.Validate.parse("#ffffff")).toEqual("#ffffff") 78 | expect.string(WCAG.Validate.parse("#0088FF")).toEqual("#0088FF") 79 | expect.string(WCAG.Validate.parse("ffffff")).toEqual("#ffffff") 80 | }) 81 | 82 | test("parses rgb colors", ({expect}) => { 83 | expect.string(WCAG.Validate.parse("rgb(255,255,255)")).toEqual("rgb(255,255,255)") 84 | expect.string(WCAG.Validate.parse("rgb(255, 255, 255)")).toEqual("rgb(255, 255, 255)") 85 | }) 86 | 87 | test("parses hsl colors", ({expect}) => { 88 | expect.string(WCAG.Validate.parse("hsl(255,30%,100%)")).toEqual("hsl(255,30%,100%)") 89 | expect.string(WCAG.Validate.parse("hsl(360, 30, 80%)")).toEqual("hsl(360, 30, 80%)") 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const outputDir = path.join(__dirname, "dist/"); 4 | 5 | const isProd = process.env.NODE_ENV === "production"; 6 | 7 | module.exports = { 8 | mode: isProd ? "production" : "development", 9 | output: { 10 | path: outputDir, 11 | filename: "index.[fullhash].js", 12 | publicPath: "/", 13 | }, 14 | plugins: [ 15 | new HtmlWebpackPlugin({ 16 | template: "public/index.html", 17 | }), 18 | ], 19 | devServer: { 20 | compress: true, 21 | contentBase: outputDir, 22 | port: process.env.PORT || 3000, 23 | historyApiFallback: true, 24 | stats: "minimal", 25 | }, 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.css$/, 30 | use: [ 31 | "style-loader", 32 | { loader: "css-loader", options: { importLoaders: 1 } }, 33 | "postcss-loader", 34 | ], 35 | }, 36 | ], 37 | }, 38 | }; 39 | --------------------------------------------------------------------------------