[WEB-5305] refactor: migrate emoji and logo components to propel and remove duplication (#8038)

This commit is contained in:
Anmol Singh Bhatia 2025-11-06 17:33:06 +05:30 committed by GitHub
parent 4927ef8c71
commit 9b3bd11c03
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 62 additions and 858 deletions

View file

@ -44,8 +44,8 @@
"@plane/hooks": "workspace:*",
"@plane/types": "workspace:*",
"@plane/ui": "workspace:*",
"@plane/utils": "workspace:*",
"@plane/propel": "workspace:*",
"@plane/utils": "workspace:*",
"@tiptap/core": "catalog:",
"@tiptap/extension-blockquote": "^2.22.3",
"@tiptap/extension-character-count": "^2.22.3",

View file

@ -1,6 +1,7 @@
// plane imports
import { EmojiIconPicker, EmojiIconPickerTypes, Logo, TEmojiLogoProps } from "@plane/ui";
import { cn, convertHexEmojiToDecimal } from "@plane/utils";
import { EmojiPicker, EmojiIconPickerTypes, Logo } from "@plane/propel/emoji-icon-picker";
import type { TLogoProps } from "@plane/types";
import { cn } from "@plane/utils";
// types
import { TCalloutBlockAttributes } from "./types";
// utils
@ -17,7 +18,7 @@ type Props = {
export const CalloutBlockLogoSelector: React.FC<Props> = (props) => {
const { blockAttributes, disabled, handleOpen, isOpen, updateAttributes } = props;
const logoValue: TEmojiLogoProps = {
const logoValue: TLogoProps = {
in_use: blockAttributes["data-logo-in-use"],
icon: {
color: blockAttributes["data-icon-color"],
@ -31,7 +32,7 @@ export const CalloutBlockLogoSelector: React.FC<Props> = (props) => {
return (
<div contentEditable={false}>
<EmojiIconPicker
<EmojiPicker
closeOnSelect={false}
isOpen={isOpen}
handleToggle={handleOpen}
@ -43,7 +44,7 @@ export const CalloutBlockLogoSelector: React.FC<Props> = (props) => {
onChange={(val) => {
// construct the new logo value based on the type of value
let newLogoValue: Partial<TCalloutBlockAttributes> = {};
let newLogoValueToStoreInLocalStorage: TEmojiLogoProps = {
let newLogoValueToStoreInLocalStorage: TLogoProps = {
in_use: "emoji",
emoji: {
value: DEFAULT_CALLOUT_BLOCK_ATTRIBUTES["data-emoji-unicode"],
@ -51,15 +52,16 @@ export const CalloutBlockLogoSelector: React.FC<Props> = (props) => {
},
};
if (val.type === "emoji") {
// val.value is now a string in decimal format (e.g. "128512")
newLogoValue = {
"data-emoji-unicode": convertHexEmojiToDecimal(val.value.unified),
"data-emoji-url": val.value.imageUrl,
"data-emoji-unicode": val.value,
"data-emoji-url": undefined,
};
newLogoValueToStoreInLocalStorage = {
in_use: "emoji",
emoji: {
value: convertHexEmojiToDecimal(val.value.unified),
url: val.value.imageUrl,
value: val.value,
url: undefined,
},
};
} else if (val.type === "icon") {

View file

@ -1,5 +1,5 @@
// plane imports
import type { TEmojiLogoProps } from "@plane/ui";
import type { TLogoProps } from "@plane/types";
import { sanitizeHTML } from "@plane/utils";
// types
import {
@ -33,7 +33,7 @@ export const getStoredLogo = (): TStoredLogoValue => {
if (typeof window !== "undefined") {
const storedData = sanitizeHTML(localStorage.getItem("editor-calloutComponent-logo") ?? "");
if (storedData) {
let parsedData: TEmojiLogoProps;
let parsedData: TLogoProps;
try {
parsedData = JSON.parse(storedData);
} catch (error) {
@ -65,7 +65,7 @@ export const getStoredLogo = (): TStoredLogoValue => {
return fallBackValues;
};
// function to update the stored logo on local storage
export const updateStoredLogo = (value: TEmojiLogoProps): void => {
export const updateStoredLogo = (value: TLogoProps): void => {
if (typeof window === "undefined") return;
localStorage.setItem("editor-calloutComponent-logo", JSON.stringify(value));
};

View file

@ -1,4 +1,5 @@
export * from "./emoji-picker";
export * from "./helper";
export * from "./logo";
export * from "./lucide-icons";
export * from "./material-icons";

View file

@ -1,26 +1,18 @@
"use client";
import { Emoji } from "emoji-picker-react";
import React, { FC } from "react";
import type { FC } from "react";
// Due to some weird issue with the import order, the import of useFontFaceObserver
// should be after the imported here rather than some below helper functions as it is in the original file
// eslint-disable-next-line import/order
import useFontFaceObserver from "use-font-face-observer";
// plane imports
import type { TLogoProps } from "@plane/types";
// local imports
import { LUCIDE_ICONS_LIST } from "..";
import { emojiCodeToUnicode } from "./helpers";
export type TEmojiLogoProps = {
in_use: "emoji" | "icon";
emoji?: {
value?: string;
url?: string;
};
icon?: {
name?: string;
color?: string;
};
};
import { getEmojiSize, stringToEmoji } from "./helper";
import { LUCIDE_ICONS_LIST } from "./lucide-icons";
type Props = {
logo: TEmojiLogoProps;
logo: TLogoProps;
size?: number;
type?: "lucide" | "material";
};
@ -31,9 +23,6 @@ export const Logo: FC<Props> = (props) => {
// destructuring the logo object
const { in_use, emoji, icon } = logo;
// if no in_use value, return empty fragment
if (!in_use) return <></>;
// derived values
const value = in_use === "emoji" ? emoji?.value : icon?.name;
const color = icon?.color;
@ -64,7 +53,19 @@ export const Logo: FC<Props> = (props) => {
// emoji
if (in_use === "emoji") {
return <Emoji unified={emojiCodeToUnicode(value)} size={size} />;
return (
<span
className="flex items-center justify-center"
style={{
fontSize: `${getEmojiSize(size)}rem`,
lineHeight: `${getEmojiSize(size)}rem`,
height: size,
width: size,
}}
>
{stringToEmoji(emoji?.value || "")}
</span>
);
}
// icon

View file

@ -47,7 +47,6 @@
"@popperjs/core": "^2.11.8",
"@radix-ui/react-scroll-area": "^1.2.3",
"clsx": "^2.0.0",
"emoji-picker-react": "^4.5.16",
"lodash-es": "catalog:",
"lucide-react": "catalog:",
"react-color": "^2.19.3",

View file

@ -1,102 +0,0 @@
import { Placement } from "@popperjs/core";
import { EmojiClickData, Theme } from "emoji-picker-react";
export enum EmojiIconPickerTypes {
EMOJI = "emoji",
ICON = "icon",
}
export const TABS_LIST = [
{
key: EmojiIconPickerTypes.EMOJI,
title: "Emojis",
},
{
key: EmojiIconPickerTypes.ICON,
title: "Icons",
},
];
export type TChangeHandlerProps =
| {
type: EmojiIconPickerTypes.EMOJI;
value: EmojiClickData;
}
| {
type: EmojiIconPickerTypes.ICON;
value: {
name: string;
color: string;
};
};
export type TCustomEmojiPicker = {
isOpen: boolean;
handleToggle: (value: boolean) => void;
buttonClassName?: string;
className?: string;
closeOnSelect?: boolean;
defaultIconColor?: string;
defaultOpen?: EmojiIconPickerTypes;
disabled?: boolean;
dropdownClassName?: string;
label: React.ReactNode;
onChange: (value: TChangeHandlerProps) => void;
placement?: Placement;
searchDisabled?: boolean;
searchPlaceholder?: string;
theme?: Theme;
iconType?: "material" | "lucide";
};
export const DEFAULT_COLORS = ["#95999f", "#6d7b8a", "#5e6ad2", "#02b5ed", "#02b55c", "#f2be02", "#e57a00", "#f38e82"];
export type TIconsListProps = {
defaultColor: string;
onChange: (val: { name: string; color: string }) => void;
searchDisabled?: boolean;
};
/**
* Adjusts the given hex color to ensure it has enough contrast.
* @param {string} hex - The hex color code input by the user.
* @returns {string} - The adjusted hex color code.
*/
export const adjustColorForContrast = (hex: string): string => {
// Ensure hex color is valid
if (!/^#([0-9A-F]{3}){1,2}$/i.test(hex)) {
throw new Error("Invalid hex color code");
}
// Convert hex to RGB
let r = 0,
g = 0,
b = 0;
if (hex.length === 4) {
r = parseInt(hex[1] + hex[1], 16);
g = parseInt(hex[2] + hex[2], 16);
b = parseInt(hex[3] + hex[3], 16);
} else if (hex.length === 7) {
r = parseInt(hex[1] + hex[2], 16);
g = parseInt(hex[3] + hex[4], 16);
b = parseInt(hex[5] + hex[6], 16);
}
// Calculate luminance
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
// If the color is too light, darken it
if (luminance > 0.5) {
r = Math.max(0, r - 50);
g = Math.max(0, g - 50);
b = Math.max(0, b - 50);
}
// Convert RGB back to hex
const toHex = (value: number): string => {
const hex = value.toString(16);
return hex.length === 1 ? "0" + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
};

View file

@ -1,140 +0,0 @@
import { Popover, Tab } from "@headlessui/react";
import EmojiPicker from "emoji-picker-react";
import React, { useRef, useState } from "react";
import { usePopper } from "react-popper";
// plane helpers
import { useOutsideClickDetector } from "@plane/hooks";
// helpers
import { cn } from "../utils";
// hooks
import { EmojiIconPickerTypes, TABS_LIST, TCustomEmojiPicker } from "./emoji-icon-helper";
import { LucideIconsList } from "./lucide-icons-list";
// helpers
export const EmojiIconPicker: React.FC<TCustomEmojiPicker> = (props) => {
const {
isOpen,
handleToggle,
buttonClassName,
className,
closeOnSelect = true,
defaultIconColor = "#6d7b8a",
defaultOpen = EmojiIconPickerTypes.EMOJI,
disabled = false,
dropdownClassName,
label,
onChange,
placement = "bottom-start",
searchDisabled = false,
searchPlaceholder = "Search",
theme,
} = props;
// refs
const containerRef = useRef<HTMLDivElement>(null);
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
// popper-js
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement,
modifiers: [
{
name: "preventOverflow",
options: {
padding: 20,
},
},
],
});
// close dropdown on outside click
useOutsideClickDetector(containerRef, () => handleToggle(false));
return (
<Popover as="div" className={cn("relative", className)}>
<>
<Popover.Button as={React.Fragment}>
<button
type="button"
ref={setReferenceElement}
className={cn("outline-none", buttonClassName)}
disabled={disabled}
onClick={() => handleToggle(!isOpen)}
>
{label}
</button>
</Popover.Button>
{isOpen && (
<Popover.Panel className="fixed z-10" static>
<div
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
className={cn(
"w-80 bg-custom-background-100 rounded-md border-[0.5px] border-custom-border-300 overflow-hidden",
dropdownClassName
)}
>
<Tab.Group
ref={containerRef}
as="div"
className="h-full w-full flex flex-col overflow-hidden"
defaultIndex={TABS_LIST.findIndex((tab) => tab.key === defaultOpen)}
>
<Tab.List as="div" className="grid grid-cols-2 gap-1 p-2">
{TABS_LIST.map((tab) => (
<Tab
key={tab.key}
className={({ selected }) =>
cn("py-1 text-sm rounded border border-custom-border-200", {
"bg-custom-background-80": selected,
"hover:bg-custom-background-90 focus:bg-custom-background-90": !selected,
})
}
>
{tab.title}
</Tab>
))}
</Tab.List>
<Tab.Panels as="div" className="h-full w-full overflow-y-auto">
<Tab.Panel>
<EmojiPicker
onEmojiClick={(val) => {
onChange({
type: EmojiIconPickerTypes.EMOJI,
value: val,
});
if (closeOnSelect) handleToggle(false);
}}
height="20rem"
width="100%"
theme={theme}
searchDisabled={searchDisabled}
searchPlaceholder={searchPlaceholder}
previewConfig={{
showPreview: false,
}}
lazyLoadEmojis
/>
</Tab.Panel>
<Tab.Panel className="h-80 w-full relative overflow-hidden overflow-y-auto">
<LucideIconsList
defaultColor={defaultIconColor}
onChange={(val) => {
onChange({
type: EmojiIconPickerTypes.ICON,
value: val,
});
if (closeOnSelect) handleToggle(false);
}}
searchDisabled={searchDisabled}
/>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
</Popover.Panel>
)}
</>
</Popover>
);
};

View file

@ -1,139 +0,0 @@
import { Popover, Tab } from "@headlessui/react";
import EmojiPicker from "emoji-picker-react";
import React, { useRef, useState } from "react";
import { usePopper } from "react-popper";
// plane helpers
import { useOutsideClickDetector } from "@plane/hooks";
// components
import { cn } from "../utils";
import { EmojiIconPickerTypes, TABS_LIST, TCustomEmojiPicker } from "./emoji-icon-helper";
import { IconsList } from "./icons-list";
// helpers
// hooks
export const CustomEmojiIconPicker: React.FC<TCustomEmojiPicker> = (props) => {
const {
isOpen,
handleToggle,
buttonClassName,
className,
closeOnSelect = true,
defaultIconColor = "#6d7b8a",
defaultOpen = EmojiIconPickerTypes.EMOJI,
disabled = false,
dropdownClassName,
label,
onChange,
placement = "bottom-start",
searchDisabled = false,
searchPlaceholder = "Search",
theme,
} = props;
// refs
const containerRef = useRef<HTMLDivElement>(null);
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
// popper-js
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement,
modifiers: [
{
name: "preventOverflow",
options: {
padding: 20,
},
},
],
});
// close dropdown on outside click
useOutsideClickDetector(containerRef, () => handleToggle(false));
return (
<Popover as="div" className={cn("relative", className)}>
<>
<Popover.Button as={React.Fragment}>
<button
type="button"
ref={setReferenceElement}
className={cn("outline-none", buttonClassName)}
disabled={disabled}
onClick={() => handleToggle(!isOpen)}
>
{label}
</button>
</Popover.Button>
{isOpen && (
<Popover.Panel className="fixed z-10" static>
<div
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
className={cn(
"w-80 bg-custom-background-100 rounded-md border-[0.5px] border-custom-border-300 overflow-hidden",
dropdownClassName
)}
>
<Tab.Group
ref={containerRef}
as="div"
className="h-full w-full flex flex-col overflow-hidden"
defaultIndex={TABS_LIST.findIndex((tab) => tab.key === defaultOpen)}
>
<Tab.List as="div" className="grid grid-cols-2 gap-1 p-2">
{TABS_LIST.map((tab) => (
<Tab
key={tab.key}
className={({ selected }) =>
cn("py-1 text-sm rounded border border-custom-border-200", {
"bg-custom-background-80": selected,
"hover:bg-custom-background-90 focus:bg-custom-background-90": !selected,
})
}
>
{tab.title}
</Tab>
))}
</Tab.List>
<Tab.Panels as="div" className="h-full w-full overflow-y-auto">
<Tab.Panel>
<EmojiPicker
onEmojiClick={(val) => {
onChange({
type: EmojiIconPickerTypes.EMOJI,
value: val,
});
if (closeOnSelect) handleToggle(false);
}}
height="20rem"
width="100%"
theme={theme}
searchDisabled={searchDisabled}
searchPlaceholder={searchPlaceholder}
previewConfig={{
showPreview: false,
}}
/>
</Tab.Panel>
<Tab.Panel className="h-80 w-full relative overflow-hidden overflow-y-auto">
<IconsList
defaultColor={defaultIconColor}
onChange={(val) => {
onChange({
type: EmojiIconPickerTypes.ICON,
value: val,
});
if (closeOnSelect) handleToggle(false);
}}
searchDisabled={searchDisabled}
/>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
</Popover.Panel>
)}
</>
</Popover>
);
};

View file

@ -1,12 +0,0 @@
export const emojiCodeToUnicode = (emoji: string) => {
if (!emoji) return "";
// convert emoji code to unicode
const uniCodeEmoji = emoji
.toString()
.split("-")
.map((emoji) => parseInt(emoji, 10).toString(16))
.join("-");
return uniCodeEmoji;
};

View file

@ -1,153 +0,0 @@
"use client";
import { Search } from "lucide-react";
import React, { useEffect, useState } from "react";
// icons
import useFontFaceObserver from "use-font-face-observer";
import { InfoIcon } from "@plane/propel/icons";
import { MATERIAL_ICONS_LIST } from "..";
import { Input } from "../form-fields";
import { cn } from "../utils";
// components
// hooks
// helpers
import { DEFAULT_COLORS, TIconsListProps, adjustColorForContrast } from "./emoji-icon-helper";
export const IconsList: React.FC<TIconsListProps> = (props) => {
const { defaultColor, onChange, searchDisabled = false } = props;
// states
const [activeColor, setActiveColor] = useState(defaultColor);
const [showHexInput, setShowHexInput] = useState(false);
const [hexValue, setHexValue] = useState("");
const [isInputFocused, setIsInputFocused] = useState(false);
const [query, setQuery] = useState("");
useEffect(() => {
if (DEFAULT_COLORS.includes(defaultColor.toLowerCase())) setShowHexInput(false);
else {
setHexValue(defaultColor.slice(1, 7));
setShowHexInput(true);
}
}, [defaultColor]);
const filteredArray = MATERIAL_ICONS_LIST.filter((icon) => icon.name.toLowerCase().includes(query.toLowerCase()));
const isMaterialSymbolsFontLoaded = useFontFaceObserver([
{
family: `Material Symbols Rounded`,
style: `normal`,
weight: `normal`,
stretch: `condensed`,
},
]);
return (
<>
<div className="flex flex-col sticky top-0 bg-custom-background-100">
{!searchDisabled && (
<div className="flex items-center px-2 py-[15px] w-full ">
<div
className={`relative flex items-center gap-2 bg-custom-background-90 h-10 rounded-lg w-full px-[30px] border ${isInputFocused ? "border-custom-primary-100" : "border-transparent"}`}
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
>
<Search className="absolute left-2.5 bottom-3 h-3.5 w-3.5 text-custom-text-400" />
<Input
placeholder="Search"
value={query}
onChange={(e) => setQuery(e.target.value)}
className="text-[1rem] border-none p-0 h-full w-full "
/>
</div>
</div>
)}
<div className="grid grid-cols-9 gap-2 items-center justify-items-center px-2.5 py-1 h-9">
{showHexInput ? (
<div className="col-span-8 flex items-center gap-1 justify-self-stretch ml-2">
<span
className="h-4 w-4 flex-shrink-0 rounded-full mr-1"
style={{
backgroundColor: `#${hexValue}`,
}}
/>
<span className="text-xs text-custom-text-300 flex-shrink-0">HEX</span>
<span className="text-xs text-custom-text-200 flex-shrink-0 -mr-1">#</span>
<Input
type="text"
value={hexValue}
onChange={(e) => {
const value = e.target.value;
setHexValue(value);
if (/^[0-9A-Fa-f]{6}$/.test(value)) setActiveColor(adjustColorForContrast(`#${value}`));
}}
className="flex-grow pl-0 text-xs text-custom-text-200"
mode="true-transparent"
autoFocus
/>
</div>
) : (
DEFAULT_COLORS.map((curCol) => (
<button
key={curCol}
type="button"
className="grid place-items-center size-5"
onClick={() => {
setActiveColor(curCol);
setHexValue(curCol.slice(1, 7));
}}
>
<span className="h-4 w-4 cursor-pointer rounded-full" style={{ backgroundColor: curCol }} />
</button>
))
)}
<button
type="button"
className={cn("grid place-items-center h-4 w-4 rounded-full border border-transparent", {
"border-custom-border-400": !showHexInput,
})}
onClick={() => {
setShowHexInput((prevData) => !prevData);
setHexValue(activeColor.slice(1, 7));
}}
>
{showHexInput ? (
<span className="conical-gradient h-4 w-4 rounded-full" />
) : (
<span className="text-custom-text-300 text-[0.6rem] grid place-items-center">#</span>
)}
</button>
</div>
<div className="flex items-center gap-2 w-full pl-4 pr-3 py-1 h-6">
<InfoIcon className="h-3 w-3" />
<p className="text-xs"> Colors will be adjusted to ensure sufficient contrast.</p>
</div>
</div>
<div className="grid grid-cols-8 gap-1 px-2.5 justify-items-center mt-2">
{filteredArray.map((icon) => (
<button
key={icon.name}
type="button"
className="h-9 w-9 select-none text-lg grid place-items-center rounded hover:bg-custom-background-80"
onClick={() => {
onChange({
name: icon.name,
color: activeColor,
});
}}
>
{isMaterialSymbolsFontLoaded ? (
<span
style={{ color: activeColor }}
className="material-symbols-rounded !text-[1.25rem] !leading-[1.25rem]"
>
{icon.name}
</span>
) : (
<span className="size-5 rounded animate-pulse bg-custom-background-80" />
)}
</button>
))}
</div>
</>
);
};

View file

@ -1,4 +0,0 @@
export * from "./emoji-icon-picker-new";
export * from "./emoji-icon-picker";
export * from "./emoji-icon-helper";
export * from "./logo";

View file

@ -1,129 +0,0 @@
import { Search } from "lucide-react";
import React, { useEffect, useState } from "react";
// local imports
import { InfoIcon } from "@plane/propel/icons";
import { LUCIDE_ICONS_LIST } from "..";
import { Input } from "../form-fields";
import { cn } from "../utils";
import { DEFAULT_COLORS, TIconsListProps, adjustColorForContrast } from "./emoji-icon-helper";
export const LucideIconsList: React.FC<TIconsListProps> = (props) => {
const { defaultColor, onChange, searchDisabled = false } = props;
// states
const [activeColor, setActiveColor] = useState(defaultColor);
const [showHexInput, setShowHexInput] = useState(false);
const [hexValue, setHexValue] = useState("");
const [isInputFocused, setIsInputFocused] = useState(false);
const [query, setQuery] = useState("");
useEffect(() => {
if (DEFAULT_COLORS.includes(defaultColor.toLowerCase())) setShowHexInput(false);
else {
setHexValue(defaultColor.slice(1, 7));
setShowHexInput(true);
}
}, [defaultColor]);
const filteredArray = LUCIDE_ICONS_LIST.filter((icon) => icon.name.toLowerCase().includes(query.toLowerCase()));
return (
<>
<div className="flex flex-col sticky top-0 bg-custom-background-100">
{!searchDisabled && (
<div className="flex items-center px-2 py-[15px] w-full ">
<div
className={`relative flex items-center gap-2 bg-custom-background-90 h-10 rounded-lg w-full px-[30px] border ${isInputFocused ? "border-custom-primary-100" : "border-transparent"}`}
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
>
<Search className="absolute left-2.5 bottom-3 h-3.5 w-3.5 text-custom-text-400" />
<Input
placeholder="Search"
value={query}
onChange={(e) => setQuery(e.target.value)}
className="text-[1rem] border-none p-0 h-full w-full "
/>
</div>
</div>
)}
<div className="grid grid-cols-9 gap-2 items-center justify-items-center px-2.5 py-1 h-9">
{showHexInput ? (
<div className="col-span-8 flex items-center gap-1 justify-self-stretch ml-2">
<span
className="h-4 w-4 flex-shrink-0 rounded-full mr-1"
style={{
backgroundColor: `#${hexValue}`,
}}
/>
<span className="text-xs text-custom-text-300 flex-shrink-0">HEX</span>
<span className="text-xs text-custom-text-200 flex-shrink-0 -mr-1">#</span>
<Input
type="text"
value={hexValue}
onChange={(e) => {
const value = e.target.value;
setHexValue(value);
if (/^[0-9A-Fa-f]{6}$/.test(value)) setActiveColor(adjustColorForContrast(`#${value}`));
}}
className="flex-grow pl-0 text-xs text-custom-text-200"
mode="true-transparent"
autoFocus
/>
</div>
) : (
DEFAULT_COLORS.map((curCol) => (
<button
key={curCol}
type="button"
className="grid place-items-center size-5"
onClick={() => {
setActiveColor(curCol);
setHexValue(curCol.slice(1, 7));
}}
>
<span className="h-4 w-4 cursor-pointer rounded-full" style={{ backgroundColor: curCol }} />
</button>
))
)}
<button
type="button"
className={cn("grid place-items-center h-4 w-4 rounded-full border border-transparent", {
"border-custom-border-400": !showHexInput,
})}
onClick={() => {
setShowHexInput((prevData) => !prevData);
setHexValue(activeColor.slice(1, 7));
}}
>
{showHexInput ? (
<span className="conical-gradient h-4 w-4 rounded-full" />
) : (
<span className="text-custom-text-300 text-[0.6rem] grid place-items-center">#</span>
)}
</button>
</div>
<div className="flex items-center gap-2 w-full pl-4 pr-3 py-1 h-6">
<InfoIcon className="flex-shrink-0 h-3 w-3" />
<p className="!text-xs"> Colors will be adjusted to ensure sufficient contrast.</p>
</div>
</div>
<div className="grid grid-cols-8 gap-1 px-2.5 justify-items-center mt-2">
{filteredArray.map((icon) => (
<button
key={icon.name}
type="button"
className="h-9 w-9 select-none text-lg grid place-items-center rounded hover:bg-custom-background-80"
onClick={() => {
onChange({
name: icon.name,
color: activeColor,
});
}}
>
<icon.element style={{ color: activeColor }} className="size-4" />
</button>
))}
</div>
</>
);
};

View file

@ -12,7 +12,6 @@ export * from "./drag-handle";
export * from "./drop-indicator";
export * from "./dropdown";
export * from "./dropdowns";
export * from "./emoji";
export * from "./favorite-star";
export * from "./form-fields";
export * from "./header";