Загрузка данных
import classNames from 'classnames';
import { Button } from 'exchange-elements/v2';
import { ChangeEvent } from 'react';
import { t } from '@src/translations';
import { normalizeColor } from '@src/utils';
import styles from './index.module.scss';
import { RangeField } from './RangeField';
interface ColorPickerProps {
value: string;
onChange: (value: string) => void;
}
const COLOR_PALETTE = [
['#ffffff', '#d9d9d9', '#bfbfbf', '#a6a6a6', '#8c8c8c', '#737373', '#595959', '#404040', '#262626', '#000000'],
['#f23645', '#ff9800', '#ffeb3b', '#4caf50', '#009688', '#16b8c4', '#2962ff', '#673ab7', '#9c27b0', '#e91e63'],
['#f8c1c5', '#ffe0b2', '#fff9c4', '#c8e6c9', '#b2dfdb', '#b2ebf2', '#bbdefb', '#d1c4e9', '#e1bee7', '#f8bbd0'],
['#f58f97', '#ffcc80', '#fff59d', '#a5d6a7', '#80cbc4', '#80deea', '#90caf9', '#b39ddb', '#ce93d8', '#f48fb1'],
['#f16b75', '#ffb74d', '#ffee58', '#81c784', '#4db6ac', '#4dd0e1', '#64b5f6', '#9575cd', '#ba68c8', '#f06292'],
['#ed4c5a', '#ffa726', '#fdd835', '#66bb6a', '#26a69a', '#26c6da', '#4285e8', '#7e57c2', '#ab47bc', '#ec407a'],
['#c62835', '#f57c00', '#fbc02d', '#388e3c', '#00796b', '#0097a7', '#1f4dcc', '#5e35b1', '#8e24aa', '#c2185b'],
['#991b24', '#e65100', '#ef6c00', '#1b5e20', '#004d40', '#006064', '#173b99', '#311b92', '#4a148c', '#880e4f'],
] as const;
export function normalizeHexColor(value: string): string {
const normalized = value.trim().startsWith('#') ? normalizeColor(value) : `#${normalizeColor(value)}`;
if (/^#[0-9a-f]{6}$/i.test(normalized)) {
return `${normalized}ff`;
}
if (/^#[0-9a-f]{8}$/i.test(normalized)) {
return normalized;
}
return '#000000ff';
}
export function ColorPicker({ value, onChange }: ColorPickerProps) {
const currentColor = normalizeHexColor(value);
const rgb = currentColor.slice(0, 7).toLowerCase();
const alpha = currentColor.slice(7, 9);
const opacity = Math.round((parseInt(alpha, 16) / 255) * 100);
const handleColorChange = (color: string): void => {
onChange(`${color}${alpha}`);
};
const handleOpacityChange = (event: ChangeEvent<HTMLInputElement>): void => {
const nextOpacity = Number(event.target.value);
const nextAlpha = Math.round((nextOpacity / 100) * 255)
.toString(16)
.padStart(2, '0');
onChange(`${rgb}${nextAlpha}`);
};
return (
<div className={styles.colorPicker}>
<div className={styles.colorPalette}>
{COLOR_PALETTE.flatMap((row) =>
row.map((color) => {
const isSelected = color.toLowerCase() === rgb;
return (
<Button
key={color}
size="sm"
className={classNames(styles.colorOption, {
[styles.selected]: isSelected,
})}
style={{
['--color-option' as string]: color,
}}
aria-label={color}
aria-pressed={isSelected}
onClick={() => handleColorChange(color)}
/>
);
}),
)}
</div>
<div className={styles.colorDivider} />
<RangeField
label={t('Opacity')}
value={opacity}
color={rgb}
suffix="%"
onChange={handleOpacityChange}
/>
</div>
);
}
import classNames from 'classnames';
import { Button } from 'exchange-elements/v2';
import { Dropdown } from '@src/components/Dropdown';
import styles from './index.module.scss';
import { ColorPicker, normalizeHexColor } from './ColorPicker';
interface ColorFieldProps {
label: string;
value: string;
onChange: (value: string) => void;
}
export function ColorField({ label, value, onChange }: ColorFieldProps) {
const currentColor = normalizeHexColor(value);
return (
<div className={styles.colorField}>
<span className={styles.colorFieldLabel}>{label}</span>
<Dropdown
selectedValue={currentColor}
position="bottom"
menuClassName={styles.colorMenu}
renderTrigger={({ isOpen, onToggle }) => (
<Button
size="sm"
className={classNames(styles.colorTrigger, {
[styles.pressed]: isOpen,
})}
aria-label={label}
aria-haspopup="dialog"
aria-expanded={isOpen}
onClick={onToggle}
>
<span className={styles.colorPreview}>
<span
className={styles.colorPreviewFill}
style={{ backgroundColor: currentColor }}
/>
</span>
</Button>
)}
>
<ColorPicker
value={currentColor}
onChange={onChange}
/>
</Dropdown>
</div>
);
}
@use '../../theme/mixins' as m;
$color-option-size: 32px;
$color-menu-width: 384px;
.colorField {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-1000);
}
.colorFieldLabel {
color: var(--neutral-12);
font-size: var(--space-0875);
}
.colorTrigger {
@include m.buttonBase;
min-width: auto;
padding: var(--space-0375);
border: 1px solid var(--color-field-border);
background-color: transparent;
&:hover:not(:disabled):not(.pressed) {
border-color: var(--blue-1);
}
&.pressed {
border-color: var(--blue-1);
}
p {
padding: 0;
}
}
.colorPreview {
position: relative;
width: var(--space-2000);
height: var(--space-2000);
flex-shrink: 0;
overflow: hidden;
border: 1px solid var(--color-field-border);
border-radius: var(--space-0250);
background:
linear-gradient(45deg, var(--color-field-square) 25%, transparent 25%),
linear-gradient(-45deg, var(--color-field-square) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--color-field-square) 75%),
linear-gradient(-45deg, transparent 75%, var(--color-field-square) 75%);
background-size: var(--space-0500) var(--space-0500);
background-position:
0 0,
0 calc(var(--space-0500) / 2),
calc(var(--space-0500) / 2) calc(var(--space-0500) / -2),
calc(var(--space-0500) / -2) 0;
}
.colorPreviewFill {
position: absolute;
inset: 0;
}
.colorMenu {
--dropdown-width: #{$color-menu-width};
padding: var(--space-1000);
}
.colorPicker {
display: flex;
flex-direction: column;
gap: var(--space-1000);
}
.colorPalette {
display: grid;
grid-template-columns: repeat(10, $color-option-size);
gap: var(--space-0375);
}
.colorOption {
@include m.buttonBase;
--color-option: transparent;
position: relative;
min-width: 0;
width: $color-option-size;
height: $color-option-size;
padding: 0;
border: 0;
border-radius: var(--space-0250);
background-color: var(--color-option);
&:hover:not(:disabled):not(.selected),
&:active:not(:disabled):not(.selected) {
background-color: var(--color-option);
box-shadow: 0 0 0 2px var(--neutral-8);
}
&.selected {
background-color: var(--color-option);
box-shadow:
0 0 0 2px var(--neutral-2),
0 0 0 4px var(--blue-1);
}
p {
display: none;
}
}
.colorDivider {
width: 100%;
height: 1px;
background-color: var(--color-field-border);
}