1
0
cvsa/packages/palette/src/components/Picker/Picker.tsx

195 lines
5.5 KiB
TypeScript

import { useState, useEffect } from "react";
import { oklch, formatHex, inGamut, parse, type Oklch, rgb } from "culori";
import { Slider } from "./Slider";
import { displaySupportsP3, roundOklch } from "./utils";
export type LchChannel = "l" | "c" | "h";
export const toOklchString = (color: Oklch) => {
return `oklch(${color.l} ${color.c} ${color.h})`;
};
export type i18nKeys = LchChannel | "fallback" | "unsupported";
export type I18nProvider = (key: i18nKeys) => string;
interface PickerProps extends React.HTMLAttributes<HTMLDivElement> {
selectedColor: Oklch;
onColorChange: (value: Oklch) => void;
useP3: boolean;
i18n: I18nProvider;
}
const Preview = ({ color, i18n }: { color: Oklch; i18n: I18nProvider }) => {
const supportsP3 = displaySupportsP3();
const outOfRgb = !inGamut("rgb")(color);
const outOfP3 = !inGamut("p3")(color);
if (outOfP3 || (outOfRgb && !supportsP3)) {
const rgbColor = rgb(color);
const hex = formatHex(rgbColor);
const fallbackColor = supportsP3 ? hex : toOklchString(color);
return (
<div className="flex gap-1">
<div
className="w-full h-20 mb-5 rounded-lg flex items-center justify-center
border border-dashed border-gray-300 dark:border-gray-500"
>
<span className="text-gray-700 dark:text-zinc-300 text-sm text-center">{i18n("unsupported")}</span>
</div>
<div
className="w-full h-20 mb-5 rounded-lg flex items-end justify-center"
style={{ backgroundColor: fallbackColor }}
>
<span className="text-sm mb-1 bg-black/70 text-white px-2 rounded-xl">{i18n("fallback")}</span>
</div>
</div>
);
} else if (outOfRgb && supportsP3) {
const rgbColor = rgb(color);
const hex = formatHex(rgbColor);
return (
<div className="flex gap-1">
<div
className="w-full h-20 mb-5 rounded-lg flex items-end justify-center"
style={{ backgroundColor: toOklchString(color) }}
>
<span className="text-sm mb-1 bg-black/70 text-white px-2 rounded-xl">P3</span>
</div>
<div
className="w-full h-20 mb-5 rounded-lg flex items-end justify-center"
style={{ backgroundColor: hex }}
>
<span className="text-sm mb-1 bg-black/70 text-white px-2 rounded-xl">{i18n("fallback")}</span>
</div>
</div>
);
}
return (
<div
className="w-full h-20 mb-5 rounded-lg flex items-center justify-center"
style={{ backgroundColor: toOklchString(color) }}
></div>
);
};
export const Picker = ({ useP3, i18n, selectedColor, onColorChange, ...rest }: PickerProps) => {
const [displayColor, setDisplayColor] = useState<Oklch>(selectedColor);
const [hexText, setHexText] = useState(formatHex(selectedColor));
const [oklchText, setOklchText] = useState(toOklchString(selectedColor));
useEffect(() => {
try {
setHexText(formatHex(selectedColor));
setOklchText(toOklchString(selectedColor));
setDisplayColor(selectedColor);
} catch (error) {
console.warn("Invalid color combination");
}
}, [selectedColor]);
const handleChange = (channel: LchChannel, value: number) => {
setDisplayColor((prev) => ({ ...prev, [channel]: value }));
onColorChange({ ...selectedColor, [channel]: value });
};
const generateChangeHandler = (channel: LchChannel) => {
return (value: number) => {
handleChange(channel, value);
};
};
const handleHexChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const hex = e.target.value;
const color = parse(hex);
const oklchColor = oklch(rgb(color));
if (oklchColor) {
setDisplayColor(roundOklch(oklchColor));
}
setHexText(hex);
};
const handleHexInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const hex = e.target.value;
const color = parse(hex);
const oklchColor = oklch(rgb(color));
if (oklchColor) {
onColorChange(roundOklch(oklchColor));
setHexText(hex);
} else {
setHexText(formatHex(selectedColor));
}
};
const handleOklchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const oklchString = e.target.value;
const color = parse(oklchString);
const oklchColor = oklch(color);
if (oklchColor) {
setDisplayColor(roundOklch(oklchColor));
}
setOklchText(oklchString);
};
const handleOklchInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const oklchString = e.target.value;
const oklchColor = oklch(parse(oklchString));
if (oklchColor) {
onColorChange(roundOklch(oklchColor));
setOklchText(oklchString);
} else {
setOklchText(toOklchString(selectedColor));
}
};
return (
<div {...rest}>
<Preview color={displayColor} i18n={i18n} />
<div className="flex flex-col">
<input
className="mb-5 font-mono bg-zinc-200 dark:bg-zinc-700 h-10 px-3 rounded-xl focus:outline-none
focus:ring focus:ring-2 focus:ring-black dark:focus:ring-white"
type="text"
value={hexText}
onChange={handleHexChange}
onBlur={handleHexInput}
id="colorHex"
/>
<input
className="mb-5 font-mono bg-zinc-200 dark:bg-zinc-700 h-10 px-3 rounded-xl focus:outline-none
focus:ring focus:ring-2 focus:ring-black dark:focus:ring-white"
type="text"
value={oklchText}
onChange={handleOklchChange}
onBlur={handleOklchInput}
id="colorOklch"
/>
</div>
<Slider
channel="l"
color={displayColor}
onChange={generateChangeHandler("l")}
i18nProvider={i18n}
useP3={useP3}
/>
<Slider
channel="c"
color={displayColor}
onChange={generateChangeHandler("c")}
i18nProvider={i18n}
useP3={useP3}
/>
<Slider
channel="h"
color={displayColor}
onChange={generateChangeHandler("h")}
i18nProvider={i18n}
useP3={useP3}
/>
</div>
);
};