Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ label {
text-align: right;
}

.input-and-copy-button {
position: relative;
}

input[type="text"] {
width: var(--input-width);
font-size: clamp(1rem, 2vw, 2rem);
Expand All @@ -74,6 +78,36 @@ input[type="text"] {
background-color ease-in 0.2s;
}

.copy-button {
font-family: inherit;
font-size: clamp(0.6rem, 1.2vw, 1.2rem);
padding: 0.3em 0.6em;
margin-left: 0.5vw;
cursor: pointer;
border: 2px solid var(--black);
background: white;
border-radius: clamp(0.5rem, 1vw, 1rem);
transition: background-color ease-in 0.2s;
vertical-align: middle;
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 1vw;
}

.copy-button:hover {
background: var(--gray);
}

.copy-button:active {
transform: scale(0.95) translateY(-50%);
}

/* hide the copy button if the input is empty */
input[type="text"][value=""] + .copy-button {
display: none;
}

.color-input-wrapper {
width: var(--input-width);
display: inline-block;
Expand Down
18 changes: 0 additions & 18 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,6 @@ const App: React.FC = () => {
incomingColorType={incomingColorType}
/>

<Input
labelText="rgba"
colorType={colorTypes.rgba}
placeHolder="rgba(255 0 0 / 1)"
onChange={onInputChange}
incomingColor={incomingColor}
incomingColorType={incomingColorType}
/>

<Input
labelText="p3"
colorType={colorTypes.p3}
Expand All @@ -89,15 +80,6 @@ const App: React.FC = () => {
incomingColorType={incomingColorType}
/>

<Input
labelText="hsla"
colorType={colorTypes.hsla}
placeHolder="hsla(100 100% 50% / 1)"
onChange={onInputChange}
incomingColor={incomingColor}
incomingColorType={incomingColorType}
/>

<Input
labelText="lch"
colorType={colorTypes.lch}
Expand Down
53 changes: 40 additions & 13 deletions src/components/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect, ChangeEvent } from "react";
import React, { useState, useEffect, useRef, ChangeEvent } from "react";

import { colorTypes } from "../utils/colorTypes";
import { isOutOfSrgbGamut } from "../utils/isOutOfSrgbGamut";
Expand Down Expand Up @@ -37,7 +37,10 @@ const Input: React.FC<InputProps> = (props) => {
} = props;

const getOutOfFocusState = (colorValue: string) => {
if ([colorTypes.lch, colorTypes.oklch, colorTypes.p3].includes(colorType) && isOutOfSrgbGamut(colorValue)) {
if (
[colorTypes.lch, colorTypes.oklch, colorTypes.p3].includes(colorType) &&
isOutOfSrgbGamut(colorValue)
) {
return inputStates.outOfFocusOutOfGamut;
}
return inputStates.outOfFocus;
Expand All @@ -47,13 +50,31 @@ const Input: React.FC<InputProps> = (props) => {

const [value, setValue] = useState(incomingColor);
const [inputState, setInputState] = useState(initInputState());
const [copied, setCopied] = useState(false);
const copyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

const copyHandler = () => {
navigator.clipboard.writeText(value);
setCopied(true);
if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current);
copyTimeoutRef.current = setTimeout(() => setCopied(false), 3000);
};

useEffect(() => {
return () => {
if (copyTimeoutRef.current) clearTimeout(copyTimeoutRef.current);
};
}, []);

const localChangeHandler = (e: ChangeEvent<HTMLInputElement>) => {
const changedValue = e.currentTarget.value;
setValue(changedValue);

if (isValidColor(changedValue, colorType)) {
if ([colorTypes.lch, colorTypes.oklch, colorTypes.p3].includes(colorType) && isOutOfSrgbGamut(changedValue)) {
if (
[colorTypes.lch, colorTypes.oklch, colorTypes.p3].includes(colorType) &&
isOutOfSrgbGamut(changedValue)
) {
setInputState(inputStates.inFocusValidValueOutOfGamut);
} else {
setInputState(inputStates.inFocusValidValue);
Expand Down Expand Up @@ -94,7 +115,8 @@ const Input: React.FC<InputProps> = (props) => {
}, [translatedIncomingColor]);

if (
(inputState === inputStates.outOfFocus || inputState === inputStates.outOfFocusOutOfGamut) &&
(inputState === inputStates.outOfFocus ||
inputState === inputStates.outOfFocusOutOfGamut) &&
translatedIncomingColor !== colorTypes.none &&
translatedIncomingColor !== value
) {
Expand Down Expand Up @@ -130,15 +152,20 @@ const Input: React.FC<InputProps> = (props) => {
<div className="input-wrapper">
<label>
<span className="label-text">{labelText}:</span>
<input
type="text"
placeholder={placeHolder}
onChange={localChangeHandler}
onFocus={() => setInputState(inputStates.inFocus)}
onBlur={blurHandler}
value={value}
name={colorType}
/>
<span className="input-and-copy-button">
<input
type="text"
placeholder={placeHolder}
onChange={localChangeHandler}
onFocus={() => setInputState(inputStates.inFocus)}
onBlur={blurHandler}
value={value}
name={colorType}
/>
<button className="copy-button" onClick={copyHandler}>
{copied ? "copied!" : "copy"}
</button>
</span>
</label>
{showGamutWarning && (
<small className="gamut-warning">
Expand Down
4 changes: 2 additions & 2 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const DEFAULT_COLOR = `rgba(255 0 0 / 1)`; // red
const DEFAULT_COLOR = `rgba(255 0 0 / 60%)`; // red
const ASSUMED_BACKGROUND_COLOR = [255, 255, 255]; // white

export { DEFAULT_COLOR, ASSUMED_BACKGROUND_COLOR }
export { DEFAULT_COLOR, ASSUMED_BACKGROUND_COLOR };
2 changes: 0 additions & 2 deletions src/utils/colorTypes.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
export enum colorTypes {
rgb = `rgb`,
rgba = `rgba`,
hex6 = `hex6`,
hex8 = `hex8`,
hsl = `hsl`,
hsla = `hsla`,
lch = `lch`,
oklch = `oklch`,
p3 = `p3`,
Expand Down
52 changes: 11 additions & 41 deletions src/utils/isValidColor.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isValidColor, isValidHex6, isValidHex8, isValidRgb, isValidRgba, isValidHsl, isValidHsla, isValidLch, isValidOklch, isValidP3 } from './isValidColor';
import { isValidColor, isValidHex6, isValidHex8, isValidRgb, isValidHsl, isValidLch, isValidOklch, isValidP3 } from './isValidColor';
import { colorTypes } from './colorTypes';


Expand Down Expand Up @@ -47,6 +47,8 @@ describe('isValidRgb', () => {
expect(isValidRgb('rgb(255 255 255)')).toBe(true);
expect(isValidRgb('rgb(0 0 0)')).toBe(true);
expect(isValidRgb('rgb(100% 100% 100%)')).toBe(true);
expect(isValidRgb('rgb(255 0 0 / 0.5)')).toBe(true);
expect(isValidRgb('rgba(255 0 0 / 0.5)')).toBe(true);
});

it('return false on invalid rgb values', () => {
Expand All @@ -55,25 +57,6 @@ describe('isValidRgb', () => {
});
});

describe('isValidRgba', () => {
it('return true on valid rgba values', () => {
expect(isValidRgba('rgba(100%, 100%, 100%, 1)')).toBe(true);
expect(isValidRgba('rgba(255, 255, 255, 0.5)')).toBe(true);
expect(isValidRgba('rgba(255,255,255,0.5)')).toBe(true);
});

it('return true on valid modern syntax rgba values', () => {
expect(isValidRgba('rgba(255 255 255 / 0.5)')).toBe(true);
expect(isValidRgba('rgba(0 0 0 / 1)')).toBe(true);
expect(isValidRgba('rgba(100% 100% 100% / 0.5)')).toBe(true);
});

it('return false on invalid rgba values', () => {
expect(isValidRgba('rgb(100%, 100%, 100%)')).toBe(false);
expect(isValidRgba('hsl(100, 100%, 100%)')).toBe(false);
});
});

describe('isValidHsl', () => {
it('return true on valid hsl values', () => {
expect(isValidHsl('hsl(100, 100%, 100%)')).toBe(true);
Expand All @@ -83,6 +66,8 @@ describe('isValidHsl', () => {
it('return true on valid modern syntax hsl values', () => {
expect(isValidHsl('hsl(100 100% 100%)')).toBe(true);
expect(isValidHsl('hsl(0 0% 0%)')).toBe(true);
expect(isValidHsl('hsl(0 100% 50% / 0.5)')).toBe(true);
expect(isValidHsl('hsla(0 100% 50% / 0.5)')).toBe(true);
});

it('return false on invalid hsl values', () => {
Expand All @@ -91,23 +76,6 @@ describe('isValidHsl', () => {
});
});

describe('isValidHsla', () => {
it('return true on valid hsla values', () => {
expect(isValidHsla('hsla(100, 100%, 100%, 0.5)')).toBe(true);
expect(isValidHsla('hsla(100,100%,100%, 1)')).toBe(true);
});

it('return true on valid modern syntax hsla values', () => {
expect(isValidHsla('hsla(100 100% 100% / 0.5)')).toBe(true);
expect(isValidHsla('hsla(0 0% 0% / 1)')).toBe(true);
});

it('return false on invalid hsla values', () => {
expect(isValidHsla('hsla(100%, 100%, 100%)')).toBe(false);
expect(isValidHsla('hsla (100%, 255, 100%, 1)')).toBe(false);
});
});

describe('isValidLch', () => {
it('return true on valid lch values', () => {
expect(isValidLch('lch(100% 100 100 / 0.5)')).toBe(true);
Expand Down Expand Up @@ -159,18 +127,20 @@ describe('isValidColor', () => {
expect(isValidColor('purple', colorTypes.named)).toBe(true);
expect(isValidColor('#ffffff', colorTypes.hex6)).toBe(true);
expect(isValidColor('#ffffffff', colorTypes.hex8)).toBe(true);
expect(isValidColor('hsla(100, 100%, 100%, 0.5)', colorTypes.hsla)).toBe(true);
expect(isValidColor('hsl(100, 100%, 100%)', colorTypes.hsl)).toBe(true);
expect(isValidColor('rgba(100, 100, 100, 0.5)', colorTypes.rgba)).toBe(true);
expect(isValidColor('hsl(100 100% 50% / 0.5)', colorTypes.hsl)).toBe(true);
expect(isValidColor('hsla(100, 100%, 50%, 0.5)', colorTypes.hsl)).toBe(true);
expect(isValidColor('hsla(100 100% 50% / 0.5)', colorTypes.hsl)).toBe(true);
expect(isValidColor('rgb(100, 100, 100)', colorTypes.rgb)).toBe(true);
expect(isValidColor('rgb(100 100 100 / 0.5)', colorTypes.rgb)).toBe(true);
Comment thread
chipcullen marked this conversation as resolved.
expect(isValidColor('rgba(100, 100, 100, 0.5)', colorTypes.rgb)).toBe(true);
expect(isValidColor('rgba(100 100 100 / 0.5)', colorTypes.rgb)).toBe(true);
});

it('return false for invalid colors', () => {
expect(isValidColor('purplee', colorTypes.named)).toBe(false);
expect(isValidColor('#ffffff', colorTypes.hex8)).toBe(false);
expect(isValidColor('#fffffff', colorTypes.hex6)).toBe(false);
expect(isValidColor('hsla(100, 100%, 100%, 0.5)', colorTypes.rgba)).toBe(false);
expect(isValidColor('rgb(100, 100, 100)', colorTypes.hsl)).toBe(false);
expect(isValidColor('rgb(100, 100, 100)', colorTypes.rgba)).toBe(false);
});
});
38 changes: 16 additions & 22 deletions src/utils/isValidColor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Color from 'colorjs.io';
import { colorTypes } from './colorTypes';
import { lowerCaseNamedColors } from './namedColors';
import Color from "colorjs.io";
import { colorTypes } from "./colorTypes";
import { lowerCaseNamedColors } from "./namedColors";

const canParseColor = (color: string): boolean => {
try {
Expand All @@ -21,32 +21,32 @@ const isValidHex8 = (color: string): boolean => {
return /^(#)?[0-9A-F]{4}$/i.test(color) || /^(#)?[0-9A-F]{8}$/i.test(color);
};

Comment thread
chipcullen marked this conversation as resolved.
// accepts either modern rgb/rgba syntax
const isValidRgb = (color: string): boolean => {
return color.startsWith('rgb(') && canParseColor(color);
};

const isValidRgba = (color: string): boolean => {
return color.startsWith('rgba(') && canParseColor(color);
return (
(color.startsWith("rgb(") || color.startsWith("rgba(")) &&
canParseColor(color)
);
};

Comment thread
chipcullen marked this conversation as resolved.
// accepts either modern hsl/hsla syntax
const isValidHsl = (color: string): boolean => {
return color.startsWith('hsl(') && canParseColor(color);
};

const isValidHsla = (color: string): boolean => {
return color.startsWith('hsla(') && canParseColor(color);
return (
(color.startsWith("hsl(") || color.startsWith("hsla(")) &&
canParseColor(color)
);
};

const isValidLch = (color: string): boolean => {
return color.startsWith('lch(') && canParseColor(color);
return color.startsWith("lch(") && canParseColor(color);
};

const isValidOklch = (color: string): boolean => {
return color.startsWith('oklch(') && canParseColor(color);
return color.startsWith("oklch(") && canParseColor(color);
};

const isValidP3 = (color: string): boolean => {
return color.startsWith('color(display-p3') && canParseColor(color);
return color.startsWith("color(display-p3") && canParseColor(color);
};

const isValidColor = (color: string, colorType: colorTypes): boolean => {
Expand All @@ -58,12 +58,8 @@ const isValidColor = (color: string, colorType: colorTypes): boolean => {
return isValidHex8(color);
case colorTypes.rgb:
return isValidRgb(color);
case colorTypes.rgba:
return isValidRgba(color);
case colorTypes.hsl:
return isValidHsl(color);
case colorTypes.hsla:
return isValidHsla(color);
case colorTypes.lch:
return isValidLch(color);
case colorTypes.oklch:
Expand All @@ -82,9 +78,7 @@ export {
isValidHex6,
isValidHex8,
isValidRgb,
isValidRgba,
isValidHsl,
isValidHsla,
isValidLch,
isValidOklch,
isValidP3,
Expand Down
Loading
Loading