FIX: Split the color picker in two

This commit is contained in:
Nicolás Hatcher
2025-03-21 13:25:37 +01:00
committed by Nicolás Hatcher Andrés
parent 5683d02b93
commit 155f891f8b
2 changed files with 373 additions and 307 deletions

View File

@@ -0,0 +1,292 @@
import styled from "@emotion/styled";
import { Popover, type PopoverOrigin } from "@mui/material";
import { Check } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { HexColorInput, HexColorPicker } from "react-colorful";
import { useTranslation } from "react-i18next";
import { theme } from "../../theme";
type AdvancedColorPickerProps = {
color: string;
onAccept: (color: string) => void;
onCancel: () => void;
anchorEl: React.RefObject<HTMLElement | null>;
anchorOrigin: PopoverOrigin;
transformOrigin: PopoverOrigin;
open: boolean;
};
const AdvancedColorPicker = ({
color,
onAccept,
onCancel,
anchorEl,
anchorOrigin,
transformOrigin,
open,
}: AdvancedColorPickerProps) => {
const [selectedColor, setSelectedColor] = useState<string>(color);
const recentColors = useRef<string[]>([]);
const { t } = useTranslation();
useEffect(() => {
setSelectedColor(color);
}, [color]);
const handleColorSelect = (color: string) => {
if (!recentColors.current.includes(color)) {
const maxRecentColors = 14;
recentColors.current = [color, ...recentColors.current].slice(
0,
maxRecentColors,
);
}
setSelectedColor(color);
onAccept(color);
};
return (
<StylePopover
open={open}
onClose={onCancel}
anchorEl={anchorEl.current}
anchorOrigin={anchorOrigin}
transformOrigin={transformOrigin}
>
<ColorPickerDialog>
<HexColorPicker
color={selectedColor}
onChange={(newColor): void => {
setSelectedColor(newColor);
}}
/>
<HorizontalDivider />
<ColorPickerInput>
<HexWrapper>
<HexLabel>{"Hex"}</HexLabel>
<HexColorInputBox>
<HashLabel>{"#"}</HashLabel>
<StyledHexColorInput
color={selectedColor}
onChange={(newColor): void => {
setSelectedColor(newColor);
}}
tabIndex={0}
/>
</HexColorInputBox>
</HexWrapper>
<Swatch $color={selectedColor} />
</ColorPickerInput>
<HorizontalDivider />
<Buttons>
<CancelButton onClick={onCancel}>
{t("color_picker.cancel")}
</CancelButton>
<StyledButton
onClick={(): void => {
handleColorSelect(selectedColor);
onCancel();
}}
>
<Check />
{t("color_picker.apply")}
</StyledButton>
</Buttons>
</ColorPickerDialog>
</StylePopover>
);
};
const StylePopover = styled(Popover)`
& .MuiPaper-root {
border-radius: 8px;
padding: 0px;
margin-left: -4px;
max-width: 220px;
}
`;
const HorizontalDivider = styled.div`
height: 0px;
width: 100%;
border-top: 1px solid ${theme.palette.grey["200"]};
`;
// Color Picker Dialog Styles
const ColorPickerDialog = styled.div`
background: ${theme.palette.background.default};
width: 240px;
padding: 0px;
display: flex;
flex-direction: column;
max-width: 100%;
& .react-colorful {
height: 160px;
width: 100%;
}
& .react-colorful__saturation {
border-bottom: none;
border-radius: 8px 8px 0px 0px;
}
& .react-colorful__hue {
height: 8px;
margin: 8px;
border-radius: 5px;
}
& .react-colorful__saturation-pointer {
width: 14px;
height: 14px;
}
& .react-colorful__hue-pointer {
width: 7px;
border-radius: 8px;
height: 16px;
width: 16px;
}
`;
const Buttons = styled.div`
display: flex;
justify-content: flex-end;
margin: 8px;
gap: 8px;
`;
const StyledButton = styled("div")`
cursor: pointer;
width: 100%;
color: ${theme.palette.primary.contrastText};
background: ${theme.palette.primary.main};
padding: 0px 10px;
height: 28px;
border-radius: 4px;
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
font-family: "Inter";
font-size: 12px;
&:hover {
background: #d68742;
}
svg {
max-width: 12px;
max-height: 12px;
}
`;
const CancelButton = styled("div")`
cursor: pointer;
width: 100%;
color: ${theme.palette.grey[700]};
background: ${theme.palette.grey[200]};
padding: 0px 10px;
height: 28px;
border-radius: 4px;
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
font-family: "Inter";
font-size: 12px;
&:hover {
background: ${theme.palette.grey[300]};
}
svg {
max-width: 12px;
max-height: 12px;
}
`;
const HashLabel = styled.div`
margin: auto 0px auto 10px;
font-size: 13px;
color: #333;
font-family: ${theme.typography.button.fontFamily};
`;
const HexLabel = styled.div`
margin: auto 0px;
font-size: 12px;
display: inline-flex;
font-family: ${theme.typography.button.fontFamily};
`;
const HexColorInputBox = styled.div`
display: inline-flex;
flex-grow: 1;
width: 100%;
height: 28px;
border: 1px solid ${theme.palette.grey["300"]};
border-radius: 5px;
&:hover {
border: 1px solid ${theme.palette.grey["600"]};
}
&:focus-within {
outline: 2px solid ${theme.palette.secondary.main};
outline-offset: 1px;
}
`;
const StyledHexColorInput = styled(HexColorInput)`
width: 100%;
border: none;
background: transparent;
outline: none;
font-family: ${theme.typography.button.fontFamily};
font-size: 12px;
text-transform: uppercase;
text-align: right;
padding-right: 10px;
border-radius: 5px;
&:focus {
border-color: #4298ef;
}
`;
const HexWrapper = styled.div`
display: flex;
gap: 8px;
flex-grow: 1;
& input {
min-width: 0px;
border: 0px;
background: ${theme.palette.background.default};
outline: none;
font-family: ${theme.typography.button.fontFamily};
font-size: 12px;
text-transform: uppercase;
text-align: right;
padding-right: 10px;
border-radius: 5px;
}
& input:focus {
border-color: #4298ef;
}
`;
const Swatch = styled.div<{ $color: string }>`
display: inline-flex;
${({ $color }): string => {
if ($color.toUpperCase() === "#FFFFFF") {
return `border: 1px solid ${theme.palette.grey["300"]};`;
}
return `border: 1px solid ${$color};`;
}}
background-color: ${({ $color }): string => $color};
min-width: 28px;
height: 28px;
border-radius: 5px;
`;
const ColorPickerInput = styled.div`
display: flex;
flex-direction: row;
align-items: center;
margin: 8px;
gap: 8px;
`;
export default AdvancedColorPicker;

View File

@@ -1,12 +1,11 @@
import styled from "@emotion/styled";
import { Menu, MenuItem, Popover, type PopoverOrigin } from "@mui/material";
import { Check, Plus } from "lucide-react";
import { Menu, MenuItem, type PopoverOrigin } from "@mui/material";
import { Plus } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { HexColorInput, HexColorPicker } from "react-colorful";
import { useTranslation } from "react-i18next";
import { theme } from "../../theme";
import AdvancedColorPicker from "./AdvancedColorPicker";
// Types
type ColorPickerProps = {
color: string;
defaultColor: string;
@@ -19,9 +18,6 @@ type ColorPickerProps = {
open: boolean;
};
const colorPickerWidth = 240;
// Main Component
const ColorPicker = ({
color,
defaultColor,
@@ -112,75 +108,55 @@ const ColorPicker = ({
blueTones,
];
// Render color picker or menu
if (!open) {
return null;
}
if (isPickerOpen) {
return (
<AdvancedColorPicker
color={selectedColor}
onAccept={handleColorSelect}
onCancel={() => setPickerOpen(false)}
anchorEl={anchorEl}
anchorOrigin={anchorOrigin}
transformOrigin={transformOrigin}
open={true}
/>
);
}
return (
<>
{isPickerOpen && open ? (
<StylePopover
open={isPickerOpen}
onClose={handleClose}
anchorEl={anchorEl.current}
anchorOrigin={anchorOrigin}
transformOrigin={transformOrigin}
>
<ColorPickerDialog>
<HexColorPicker
color={selectedColor}
onChange={(newColor): void => {
setSelectedColor(newColor);
<StyledMenu
anchorEl={anchorEl.current}
open={true}
onClose={handleClose}
anchorOrigin={anchorOrigin}
transformOrigin={transformOrigin}
>
<MenuItemWrapper onClick={() => handleColorSelect(defaultColor)}>
<MenuItemSquare style={{ backgroundColor: defaultColor }} />
<MenuItemText>{title}</MenuItemText>
</MenuItemWrapper>
<HorizontalDivider />
<ColorsWrapper>
<ColorList>
{mainColors.map((presetColor) => (
<ColorSwatch
key={presetColor}
$color={presetColor}
onClick={(): void => {
setSelectedColor(presetColor);
handleColorSelect(presetColor);
}}
/>
<HorizontalDivider />
<ColorPickerInput>
<HexWrapper>
<HexLabel>{"Hex"}</HexLabel>
<HexColorInputBox>
<HashLabel>{"#"}</HashLabel>
<StyledHexColorInput
color={selectedColor}
onChange={(newColor): void => {
setSelectedColor(newColor);
}}
tabIndex={0}
/>
</HexColorInputBox>
</HexWrapper>
<Swatch $color={selectedColor} />
</ColorPickerInput>
<HorizontalDivider />
<Buttons>
<CancelButton onClick={() => setPickerOpen(false)}>
{t("color_picker.cancel")}
</CancelButton>
<StyledButton
onClick={(): void => {
handleColorSelect(selectedColor);
handleClose();
}}
>
<Check />
{t("color_picker.apply")}
</StyledButton>
</Buttons>
</ColorPickerDialog>
</StylePopover>
) : (
<StyledMenu
anchorEl={anchorEl.current}
open={open && !isPickerOpen}
onClose={handleClose}
anchorOrigin={anchorOrigin}
transformOrigin={transformOrigin}
>
<MenuItemWrapper onClick={() => handleColorSelect(defaultColor)}>
<MenuItemSquare style={{ backgroundColor: defaultColor }} />
<MenuItemText>{title}</MenuItemText>
</MenuItemWrapper>
<HorizontalDivider />
<ColorsWrapper>
<ColorList>
{mainColors.map((presetColor) => (
<RecentColorButton
))}
</ColorList>
<ColorGrid>
{toneArrays.map((tones) => (
<ColorGridCol key={tones.join("-")}>
{tones.map((presetColor) => (
<ColorSwatch
key={presetColor}
$color={presetColor}
onClick={(): void => {
@@ -189,53 +165,37 @@ const ColorPicker = ({
}}
/>
))}
</ColorList>
<ColorGrid>
{toneArrays.map((tones) => (
<ColorGridCol key={tones.join("-")}>
{tones.map((presetColor) => (
<RecentColorButton
key={presetColor}
$color={presetColor}
onClick={(): void => {
setSelectedColor(presetColor);
handleColorSelect(presetColor);
}}
/>
))}
</ColorGridCol>
))}
</ColorGrid>
</ColorsWrapper>
<HorizontalDivider />
<RecentLabel>{t("color_picker.custom")}</RecentLabel>
<RecentColorsList>
{recentColors.current.length > 0 ? (
<>
{recentColors.current.map((recentColor) => (
<RecentColorButton
key={recentColor}
$color={recentColor}
onClick={(): void => {
setSelectedColor(recentColor);
handleColorSelect(recentColor);
}}
/>
))}
</>
) : (
<EmptyContainer />
)}
<StyledPlusButton
onClick={() => setPickerOpen(true)}
title={t("color_picker.add")}
>
<Plus />
</StyledPlusButton>
</RecentColorsList>
</StyledMenu>
)}
</>
</ColorGridCol>
))}
</ColorGrid>
</ColorsWrapper>
<HorizontalDivider />
<RecentLabel>{t("color_picker.custom")}</RecentLabel>
<RecentColorsList>
{recentColors.current.length > 0 ? (
<>
{recentColors.current.map((recentColor) => (
<ColorSwatch
key={recentColor}
$color={recentColor}
onClick={(): void => {
setSelectedColor(recentColor);
handleColorSelect(recentColor);
}}
/>
))}
</>
) : (
<EmptyContainer />
)}
<StyledPlusButton
onClick={() => setPickerOpen(true)}
title={t("color_picker.add")}
>
<Plus />
</StyledPlusButton>
</RecentColorsList>
</StyledMenu>
);
};
@@ -252,15 +212,6 @@ const StyledMenu = styled(Menu)`
}
`;
const StylePopover = styled(Popover)`
& .MuiPaper-root {
border-radius: 8px;
padding: 0px;
margin-left: -4px;
max-width: 220px;
}
`;
const MenuItemWrapper = styled(MenuItem)`
display: flex;
flex-direction: row;
@@ -317,7 +268,7 @@ const ColorGridCol = styled.div`
gap: 4px;
`;
const RecentColorButton = styled.button<{ $color: string }>`
const ColorSwatch = styled.button<{ $color: string }>`
width: 16px;
height: 16px;
${({ $color }): string => {
@@ -391,181 +342,4 @@ const EmptyContainer = styled.div`
display: none;
`;
// Color Picker Dialog Styles
const ColorPickerDialog = styled.div`
background: ${theme.palette.background.default};
width: ${colorPickerWidth}px;
padding: 0px;
display: flex;
flex-direction: column;
max-width: 100%;
& .react-colorful {
height: 160px;
width: 100%;
}
& .react-colorful__saturation {
border-bottom: none;
border-radius: 8px 8px 0px 0px;
}
& .react-colorful__hue {
height: 8px;
margin: 8px;
border-radius: 5px;
}
& .react-colorful__saturation-pointer {
width: 14px;
height: 14px;
}
& .react-colorful__hue-pointer {
width: 7px;
border-radius: 8px;
height: 16px;
width: 16px;
}
`;
const Buttons = styled.div`
display: flex;
justify-content: flex-end;
margin: 8px;
gap: 8px;
`;
const StyledButton = styled("div")`
cursor: pointer;
width: 100%;
color: ${theme.palette.primary.contrastText};
background: ${theme.palette.primary.main};
padding: 0px 10px;
height: 28px;
border-radius: 4px;
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
font-family: "Inter";
font-size: 12px;
&:hover {
background: #d68742;
}
svg {
max-width: 12px;
max-height: 12px;
}
`;
const CancelButton = styled("div")`
cursor: pointer;
width: 100%;
color: ${theme.palette.grey[700]};
background: ${theme.palette.grey[200]};
padding: 0px 10px;
height: 28px;
border-radius: 4px;
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
font-family: "Inter";
font-size: 12px;
&:hover {
background: ${theme.palette.grey[300]};
}
svg {
max-width: 12px;
max-height: 12px;
}
`;
const HashLabel = styled.div`
margin: auto 0px auto 10px;
font-size: 13px;
color: #333;
font-family: ${theme.typography.button.fontFamily};
`;
const HexLabel = styled.div`
margin: auto 0px;
font-size: 12px;
display: inline-flex;
font-family: ${theme.typography.button.fontFamily};
`;
const HexColorInputBox = styled.div`
display: inline-flex;
flex-grow: 1;
width: 100%;
height: 28px;
border: 1px solid ${theme.palette.grey["300"]};
border-radius: 5px;
&:hover {
border: 1px solid ${theme.palette.grey["600"]};
}
&:focus-within {
outline: 2px solid ${theme.palette.secondary.main};
outline-offset: 1px;
}
`;
const StyledHexColorInput = styled(HexColorInput)`
width: 100%;
border: none;
background: transparent;
outline: none;
font-family: ${theme.typography.button.fontFamily};
font-size: 12px;
text-transform: uppercase;
text-align: right;
padding-right: 10px;
border-radius: 5px;
&:focus {
border-color: #4298ef;
}
`;
const HexWrapper = styled.div`
display: flex;
gap: 8px;
flex-grow: 1;
& input {
min-width: 0px;
border: 0px;
background: ${theme.palette.background.default};
outline: none;
font-family: ${theme.typography.button.fontFamily};
font-size: 12px;
text-transform: uppercase;
text-align: right;
padding-right: 10px;
border-radius: 5px;
}
& input:focus {
border-color: #4298ef;
}
`;
const Swatch = styled.div<{ $color: string }>`
display: inline-flex;
${({ $color }): string => {
if ($color.toUpperCase() === "#FFFFFF") {
return `border: 1px solid ${theme.palette.grey["300"]};`;
}
return `border: 1px solid ${$color};`;
}}
background-color: ${({ $color }): string => $color};
min-width: 28px;
height: 28px;
border-radius: 5px;
`;
const ColorPickerInput = styled.div`
display: flex;
flex-direction: row;
align-items: center;
margin: 8px;
gap: 8px;
`;
export default ColorPicker;