[WEB-3540] dev: color picker component (#6823)

* dev: color picker component added

* chore: helper function added

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor
This commit is contained in:
Anmol Singh Bhatia
2025-03-27 17:48:39 +05:30
committed by GitHub
parent 471fefce8b
commit 99dba80d19
6 changed files with 153 additions and 28 deletions

View File

@@ -0,0 +1,36 @@
import * as React from "react";
interface ColorPickerProps {
value: string;
onChange: (color: string) => void;
className?: string;
}
export const ColorPicker: React.FC<ColorPickerProps> = (props) => {
const { value, onChange, className = "" } = props;
// refs
const inputRef = React.useRef<HTMLInputElement>(null);
// handlers
const handleOnClick = () => {
inputRef.current?.click();
};
return (
<div className="flex items-center justify-center relative">
<button
className={`size-4 rounded-full cursor-pointer conical-gradient ${className}`}
onClick={handleOnClick}
aria-label="Open color picker"
/>
<input
ref={inputRef}
type="color"
value={value}
onChange={(e) => onChange(e.target.value)}
className="absolute inset-0 size-4 invisible"
aria-hidden="true"
/>
</div>
);
};

View File

@@ -0,0 +1 @@
export * from "./color-picker";

View File

@@ -31,3 +31,4 @@ export * from "./card";
export * from "./tag";
export * from "./tabs";
export * from "./calendar";
export * from "./color-picker";

100
web/helpers/theme.tsx Normal file
View File

@@ -0,0 +1,100 @@
import chroma from "chroma-js";
interface HSLColor {
h: number; // hue (0-360)
s: number; // saturation (0-100)
l: number; // lightness (0-100)
}
interface ColorAdjustmentOptions {
targetContrast?: number; // Minimum contrast ratio (4.5 for WCAG AAA, 3 for WCAG AA)
preserveHue?: boolean; // Whether to maintain the original hue
maxTries?: number; // Maximum attempts to find accessible colors
}
// Helper function to ensure color contrast compliance
const ensureAccessibleColors = (
foreground: string,
background: string,
options: ColorAdjustmentOptions = {}
): { foreground: string; background: string } => {
const {
targetContrast = 4.5, // WCAG AAA by default
preserveHue = true,
maxTries = 10,
} = options;
try {
const fg = chroma(foreground);
const bg = chroma(background);
let contrast = chroma.contrast(fg, bg);
// If contrast is already good, return original colors
if (contrast >= targetContrast) {
return { foreground, background };
}
// Adjust colors to meet contrast requirements
let adjustedFg = fg;
let adjustedBg = bg;
let tries = 0;
while (contrast < targetContrast && tries < maxTries) {
if (fg.luminance() > bg.luminance()) {
// Make foreground lighter and background darker
adjustedFg = preserveHue ? fg.luminance(Math.min(fg.luminance() + 0.1, 0.9)) : fg.brighten(0.5);
adjustedBg = preserveHue ? bg.luminance(Math.max(bg.luminance() - 0.1, 0.1)) : bg.darken(0.5);
} else {
// Make foreground darker and background lighter
adjustedFg = preserveHue ? fg.luminance(Math.max(fg.luminance() - 0.1, 0.1)) : fg.darken(0.5);
adjustedBg = preserveHue ? bg.luminance(Math.min(bg.luminance() + 0.1, 0.9)) : bg.brighten(0.5);
}
contrast = chroma.contrast(adjustedFg, adjustedBg);
tries++;
}
return {
foreground: adjustedFg.css(),
background: adjustedBg.css(),
};
} catch (error) {
console.warn("Color adjustment failed:", error);
return { foreground, background };
}
};
// background color
export const createBackgroundColor = (hsl: HSLColor, resolvedTheme: "light" | "dark" = "light"): string => {
const baseColor = chroma.hsl(hsl.h, hsl.s / 100, hsl.l / 100);
// Set base opacity according to theme
const baseOpacity = resolvedTheme === "dark" ? 0.25 : 0.15;
// Create semi-transparent background
let backgroundColor = baseColor.alpha(baseOpacity);
if (hsl.l > 90) {
backgroundColor = baseColor.darken(1).alpha(resolvedTheme === "dark" ? 0.3 : 0.2);
} else if (hsl.l > 70) {
backgroundColor = baseColor.darken(0.5).alpha(resolvedTheme === "dark" ? 0.28 : 0.18);
} else if (hsl.l < 30) {
backgroundColor = baseColor.brighten(0.5).alpha(resolvedTheme === "dark" ? 0.22 : 0.12);
}
return backgroundColor.css();
};
// foreground color
export const getIconColor = (hsl: HSLColor): string => {
const baseColor = chroma.hsl(hsl.h, hsl.s / 100, hsl.l / 100);
const backgroundColor = createBackgroundColor(hsl);
// Adjust colors for accessibility
const { foreground } = ensureAccessibleColors(baseColor.css(), backgroundColor, {
targetContrast: 3, // WCAG AA for UI components
preserveHue: true,
});
return foreground;
};

View File

@@ -39,7 +39,9 @@
"@plane/utils": "*",
"@popperjs/core": "^2.11.8",
"@react-pdf/renderer": "^3.4.5",
"@types/chroma-js": "^3.1.1",
"axios": "^1.8.3",
"chroma-js": "^3.1.2",
"clsx": "^2.0.0",
"cmdk": "^1.0.0",
"comlink": "^4.4.1",

View File

@@ -3262,6 +3262,11 @@
"@types/connect" "*"
"@types/node" "*"
"@types/chroma-js@^3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-3.1.1.tgz#92cac57fb32d642ce156dbc4c052b5e3a3a25db1"
integrity sha512-SFCr4edNkZ1bGaLzGz7rgR1bRzVX4MmMxwsIa3/Bh6ose8v+hRpneoizHv0KChdjxaXyjRtaMq7sCuZSzPomQA==
"@types/compression@^1.7.5":
version "1.7.5"
resolved "https://registry.npmjs.org/@types/compression/-/compression-1.7.5.tgz#0f80efef6eb031be57b12221c4ba6bc3577808f7"
@@ -4861,6 +4866,11 @@ chownr@^1.1.1:
resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
chroma-js@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-3.1.2.tgz#cfb807045182228574eae5380587cdb830e985d6"
integrity sha512-IJnETTalXbsLx1eKEgx19d5L6SRM7cH4vINw/99p/M11HCuXGRWL+6YmCm7FWFGIo6dtWuQoQi1dc5yQ7ESIHg==
chromatic@^11.4.0:
version "11.25.2"
resolved "https://registry.npmjs.org/chromatic/-/chromatic-11.25.2.tgz#cb93dc1332d8f6b70d97a3ef126bc6d03429d396"
@@ -10722,16 +10732,7 @@ streamx@^2.15.0, streamx@^2.21.0:
optionalDependencies:
bare-events "^2.2.0"
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -10824,14 +10825,7 @@ string_decoder@^1.1.1, string_decoder@^1.3.0:
dependencies:
safe-buffer "~5.2.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -12076,16 +12070,7 @@ word-wrap@^1.2.5:
resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==