diff --git a/package.json b/package.json index 03b87ed..659bb1d 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", + "colorjs.io": "^0.6.1", "happy-dom": "^20.8.9", "query-string": "^9.3.1", "react": "^19.2.4", diff --git a/src/App.css b/src/App.css index ddcf151..4efff30 100644 --- a/src/App.css +++ b/src/App.css @@ -3,7 +3,7 @@ --swatch-size: clamp(6.25rem, 10vw, 12.5rem); --gray: #cccccc; --input-width: clamp(12.5rem, 34vw, 35rem); - --label-offset: clamp(4.5rem, 9vw, 9rem); + --label-offset: clamp(4.5rem, 9vw, 9rem); } .App { @@ -30,7 +30,7 @@ p { } .inputs, -.lch-input { +.named-input { display: flex; flex-direction: column; position: relative; @@ -38,16 +38,14 @@ p { } @media (min-width: 75rem) { - .lch-input { - left: 0; - } + .named-input { + left: 0; + } .inputs { display: block; columns: 2; left: 0; } - - } label { @@ -61,7 +59,7 @@ label { text-align: right; } -input[type=text] { +input[type="text"] { width: var(--input-width); font-size: clamp(1rem, 2vw, 2rem); padding: 1.2vw; @@ -70,7 +68,10 @@ input[type=text] { margin-bottom: 2rem; margin-left: 1vw; font-family: "Work Sans", Helvetica, sans-serif; - transition: border-color ease-in .2s, border-radius ease-in .2s, background-color ease-in .2s; + transition: + border-color ease-in 0.2s, + border-radius ease-in 0.2s, + background-color ease-in 0.2s; } .color-input-wrapper { @@ -82,19 +83,18 @@ input[type=text] { text-align: left; } -input[type=color] { +input[type="color"] { position: relative; left: -1.2vw; } input:focus { border-color: blue; - border-radius: clamp(.5rem, 1vw, 1rem);; + border-radius: clamp(0.5rem, 1vw, 1rem); outline: none; background-color: hsla(60, 100%, 50%, 0.2); } - .swatch-wrapper { --square-size: calc(var(--swatch-size) / 5); --half-square: calc(var(--swatch-size) / 10); @@ -118,14 +118,18 @@ input:focus { } .swatch-wrapper::before { - content: ''; + content: ""; display: block; position: absolute; top: 0; left: 0; width: 100%; height: 100%; - background-image: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(255,255,255,1) 70%); + background-image: linear-gradient( + 90deg, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 1) 70% + ); } .swatch { diff --git a/src/App.test.tsx b/src/App.test.tsx deleted file mode 100644 index 4ad866e..0000000 --- a/src/App.test.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; -import App from './App'; - -// @todo restore app-level testing -test.skip('renders learn react link', () => { - const { getByText } = render(); -}); diff --git a/src/App.tsx b/src/App.tsx index 5b36444..3781e7b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,21 +1,20 @@ -import * as React from 'react'; -import './App.css'; +import * as React from "react"; +import "./App.css"; import { useQueryString } from "./utils/useQueryString"; -import { colorTypes } from './utils/colorTypes'; -import { typeOfColor } from './utils/typeOfColor'; -import { colorFavicon } from './utils/colorFavicon'; -import { Swatch } from './components/Swatch'; -import { Input } from './components/Input'; -import { Footer } from './components/Footer'; -import { DEFAULT_COLOR } from './constants'; - +import { colorTypes } from "./utils/colorTypes"; +import { typeOfColor } from "./utils/typeOfColor"; +import { colorFavicon } from "./utils/colorFavicon"; +import { Swatch } from "./components/Swatch"; +import { Input } from "./components/Input"; +import { Footer } from "./components/Footer"; +import { DEFAULT_COLOR } from "./constants"; const App: React.FC = () => { const [colorQp, setColorQp] = useQueryString("color", DEFAULT_COLOR); const onInputChange = (value: string) => { setColorQp(value); - } + }; const incomingColor = colorQp ? colorQp.toString() : ``; const incomingColorType = typeOfColor(incomingColor); @@ -25,13 +24,17 @@ const App: React.FC = () => {

ColoRosetta

-

A utility to translate colors (now as a VS Code Extension!)

+

+ A utility to translate colors{" "} + + (now as a VS Code Extension!) + +

- { incomingColorType={incomingColorType} /> + + { /> + + { incomingColorType={incomingColorType} />
-
+
{
); -} +}; export default App; diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 1bbf25f..2502a0b 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,30 +1,31 @@ import React from "react"; const Footer: React.FC = () => { - return ( + © {new Date().getFullYear()}{" "} + chip cullen |{" "} + + vs code extension + {" "} + | explanatory blog post{" "} + |{" "} + + this project on github + {" "} + |{" "} + + i'm occasionally on mastodon + +
+ color conversion logic powered by{" "} + color.js - thanks to{" "} + lea verou and{" "} + chris lilley +
+ thank you to jon kanter for much of + the original conversion logic. + ); }; diff --git a/src/components/Input.tsx b/src/components/Input.tsx index f65f906..4c4b57c 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, ChangeEvent } from "react"; import { colorTypes } from "../utils/colorTypes"; -import { isLchOutOfRgbGamut } from "../utils/isLchOutOfRgbGamut"; +import { isOutOfSrgbGamut } from "../utils/isOutOfSrgbGamut"; import { isValidColor } from "../utils/isValidColor"; import { translatedColor } from "../utils/translatedColor"; @@ -36,15 +36,15 @@ const Input: React.FC = (props) => { incomingColorType, } = props; - const initInputState = () => { - // show the gamut warning on load - if (colorType === colorTypes.lch && isLchOutOfRgbGamut(incomingColor)) { + const getOutOfFocusState = (colorValue: string) => { + if ([colorTypes.lch, colorTypes.oklch, colorTypes.p3].includes(colorType) && isOutOfSrgbGamut(colorValue)) { return inputStates.outOfFocusOutOfGamut; - } else { - return inputStates.outOfFocus; } + return inputStates.outOfFocus; }; + const initInputState = () => getOutOfFocusState(incomingColor); + const [value, setValue] = useState(incomingColor); const [inputState, setInputState] = useState(initInputState()); @@ -53,7 +53,7 @@ const Input: React.FC = (props) => { setValue(changedValue); if (isValidColor(changedValue, colorType)) { - if (colorType === colorTypes.lch && isLchOutOfRgbGamut(changedValue)) { + if ([colorTypes.lch, colorTypes.oklch, colorTypes.p3].includes(colorType) && isOutOfSrgbGamut(changedValue)) { setInputState(inputStates.inFocusValidValueOutOfGamut); } else { setInputState(inputStates.inFocusValidValue); @@ -67,7 +67,7 @@ const Input: React.FC = (props) => { const blurHandler = (e: ChangeEvent) => { const changedValue = e.currentTarget.value; if (isValidColor(changedValue, colorType)) { - setInputState(inputStates.outOfFocus); + setInputState(getOutOfFocusState(changedValue)); } else { setInputState(inputStates.onBlurInvalidValue); } @@ -86,7 +86,7 @@ const Input: React.FC = (props) => { translatedIncomingColor !== value ) { setValue(translatedIncomingColor); - setInputState(inputStates.outOfFocus); + setInputState(getOutOfFocusState(translatedIncomingColor)); } // disabling this because we only want to update when // translatedIncomingColor changes, but not value or inputState @@ -94,11 +94,12 @@ const Input: React.FC = (props) => { }, [translatedIncomingColor]); if ( - inputState === inputStates.outOfFocus && + (inputState === inputStates.outOfFocus || inputState === inputStates.outOfFocusOutOfGamut) && translatedIncomingColor !== colorTypes.none && translatedIncomingColor !== value ) { setValue(translatedIncomingColor); + setInputState(getOutOfFocusState(translatedIncomingColor)); } const showGamutWarning = @@ -141,7 +142,7 @@ const Input: React.FC = (props) => { {showGamutWarning && ( - ⚠️ This lch value is outside the RGB gamut; translated values are + ⚠️ This value is outside the sRGB gamut; translated values are approximated )} diff --git a/src/components/Swatch.tsx b/src/components/Swatch.tsx index 4124c4d..54817d0 100644 --- a/src/components/Swatch.tsx +++ b/src/components/Swatch.tsx @@ -1,7 +1,6 @@ import * as React from "react"; +import Color from 'colorjs.io'; import { colorTypes } from '../utils/colorTypes'; -import { toRgba } from '../utils/toRgba'; -import { formatColor } from '../utils/formatColor'; type SwatchProps = { color: string; @@ -9,39 +8,46 @@ type SwatchProps = { }; const Swatch: React.FC = props => { - const { - color, - colorType - } = props; + const { color, colorType } = props; + + const supportsCheck: Partial> = { + [colorTypes.lch]: 'color: lch(100% 0 0)', + [colorTypes.oklch]: 'color: oklch(1 0 0)', + [colorTypes.p3]: 'color: color(display-p3 0 0 0)', + }; + + if (colorType in supportsCheck) { + let rgbaFallback = 'rgba(0 0 0 / 1)'; + try { + const c = new Color(color).toGamut({space: 'srgb'}).to('srgb'); + const [r, g, b] = c.coords.map((v: number) => Math.min(255, Math.max(0, Math.round(v * 255)))); + rgbaFallback = `rgba(${r} ${g} ${b} / ${c.alpha})`; + } catch { /* use default */ } - if (colorType === colorTypes.lch) { - // react doesn't support lch colors, so we have to use dangerouslySetInnerHTML - const rgbaFallback = formatColor(toRgba(color, colorType), colorTypes.rgba); return ( <> -
-
+
+
+