├── .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/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 |
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 |
57 | {React.string("Set as foreground color")}
58 |
59 |
62 | {React.string("Set as background color")}
63 |
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 |
--------------------------------------------------------------------------------