Skip to content

Commit e5aa646

Browse files
✨ feat: Add ColorSwatches
1 parent 2ea4d0f commit e5aa646

File tree

6 files changed

+279
-30
lines changed

6 files changed

+279
-30
lines changed

src/ColorSwatches/demos/index.tsx

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { ColorSwatches, type ColorSwatchesProps } from '@lobehub/ui';
2+
import { StoryBook, useControls, useCreateStore } from '@lobehub/ui/storybook';
3+
import { useTheme } from 'antd-style';
4+
5+
export default () => {
6+
const theme = useTheme();
7+
const store = useCreateStore();
8+
9+
const controls: ColorSwatchesProps | any = useControls(
10+
{
11+
enableColorPicker: false,
12+
enableColorSwatches: true,
13+
gap: {
14+
step: 1,
15+
value: 4,
16+
},
17+
shape: {
18+
options: ['circle', 'square'],
19+
value: 'circle',
20+
},
21+
size: {
22+
step: 1,
23+
value: 24,
24+
},
25+
},
26+
{
27+
store,
28+
},
29+
);
30+
31+
return (
32+
<StoryBook levaStore={store}>
33+
<ColorSwatches
34+
colors={[
35+
{
36+
color: '',
37+
label: 'Default',
38+
},
39+
{
40+
color: theme.red,
41+
label: 'Red',
42+
},
43+
{
44+
color: theme.orange,
45+
label: 'Orange',
46+
},
47+
{
48+
color: theme.gold,
49+
label: 'Gold',
50+
},
51+
{
52+
color: theme.yellow,
53+
label: 'Yellow',
54+
},
55+
{
56+
color: theme.lime,
57+
label: 'Lime',
58+
},
59+
{
60+
color: theme.green,
61+
label: 'Green',
62+
},
63+
{
64+
color: theme.cyan,
65+
label: 'Cyan',
66+
},
67+
{
68+
color: theme.blue,
69+
label: 'Blue',
70+
},
71+
{
72+
color: theme.geekblue,
73+
namlabele: 'Geekblue',
74+
},
75+
{
76+
color: theme.purple,
77+
label: 'Purple',
78+
},
79+
{
80+
color: theme.magenta,
81+
label: 'Magenta',
82+
},
83+
{
84+
color: theme.volcano,
85+
label: 'Volcano',
86+
},
87+
]}
88+
onChange={(color) => console.log(color)}
89+
{...controls}
90+
/>
91+
</StoryBook>
92+
);
93+
};

src/Swatches/index.md renamed to src/ColorSwatches/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
---
22
nav: Components
33
group: Data Entry
4-
title: Swatches
4+
title: ColorSwatches
55
description: The Swatches component is a memoized React component that displays a list of color swatches
66
---
77

88
## Default
99

10-
<code src="./demos/index.tsx" center></code>
10+
<code src="./demos/index.tsx" nopadding></code>
1111

1212
## APIs
1313

src/ColorSwatches/index.tsx

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
'use client';
2+
3+
import { ColorPicker } from 'antd';
4+
import { CheckIcon } from 'lucide-react';
5+
import { readableColor, rgba } from 'polished';
6+
import { memo } from 'react';
7+
import { Center, Flexbox, FlexboxProps } from 'react-layout-kit';
8+
import useMergeState from 'use-merge-value';
9+
10+
import Icon from '@/Icon';
11+
import Tooltip from '@/Tooltip';
12+
13+
import { useStyles } from './style';
14+
15+
interface ColorItem {
16+
color: string;
17+
label: string;
18+
}
19+
20+
export interface ColorSwatchesProps extends Omit<FlexboxProps, 'onChange'> {
21+
colors: ColorItem[];
22+
defaultValue?: string;
23+
enableColorPicker?: boolean;
24+
enableColorSwatches?: boolean;
25+
onChange?: (color?: string) => void;
26+
shape?: 'circle' | 'square';
27+
size?: number;
28+
texts: {
29+
custom: string;
30+
presets: string;
31+
};
32+
value?: string;
33+
}
34+
35+
const ColorSwatches = memo<ColorSwatchesProps>(
36+
({
37+
enableColorPicker,
38+
enableColorSwatches = true,
39+
defaultValue,
40+
value,
41+
style,
42+
colors,
43+
onChange,
44+
size = 24,
45+
shape = 'circle',
46+
texts,
47+
...rest
48+
}) => {
49+
const [active, setActive] = useMergeState(defaultValue, {
50+
defaultValue,
51+
onChange,
52+
value,
53+
});
54+
const { cx, styles, theme } = useStyles(size);
55+
56+
const isCustomActive =
57+
active && active !== theme.colorPrimary && !colors.some((c) => c.color === active);
58+
59+
return (
60+
<Flexbox gap={4} horizontal style={{ flexWrap: 'wrap', ...style }} {...rest}>
61+
{enableColorSwatches &&
62+
colors.map((c) => {
63+
const color = c.color || theme.colorPrimary;
64+
const isActive = (!active && !c.color) || color === active;
65+
return (
66+
<Tooltip key={c.label} title={c.label}>
67+
<Center
68+
className={cx(styles.container, isActive && styles.active)}
69+
onClick={() => setActive(color)}
70+
style={{
71+
background: color,
72+
borderRadius: shape === 'circle' ? '50%' : theme.borderRadius,
73+
}}
74+
>
75+
{isActive && (
76+
<Icon
77+
color={rgba(readableColor(color), 0.33)}
78+
icon={CheckIcon}
79+
size={{ fontSize: 14, strokeWidth: 4 }}
80+
style={{
81+
pointerEvents: 'none',
82+
}}
83+
/>
84+
)}
85+
</Center>
86+
</Tooltip>
87+
);
88+
})}
89+
{enableColorPicker && (
90+
<Tooltip title={texts?.custom || 'Custom'}>
91+
<Center style={{ position: 'relative' }}>
92+
{isCustomActive && (
93+
<Icon
94+
color={rgba(readableColor(active), 0.33)}
95+
icon={CheckIcon}
96+
size={{ fontSize: 14, strokeWidth: 4 }}
97+
style={{
98+
pointerEvents: 'none',
99+
position: 'absolute',
100+
zIndex: 1,
101+
}}
102+
/>
103+
)}
104+
<ColorPicker
105+
arrow={false}
106+
className={cx(styles.picker, isCustomActive && styles.active)}
107+
disabledAlpha
108+
format={'hex'}
109+
onChangeComplete={(c) => setActive(c.toHexString())}
110+
presets={[
111+
{
112+
colors: colors.map((c) => c.color),
113+
label: texts?.presets || 'Presets',
114+
},
115+
]}
116+
style={{
117+
borderRadius: shape === 'circle' ? '50%' : theme.borderRadius,
118+
}}
119+
value={active}
120+
/>
121+
</Center>
122+
</Tooltip>
123+
)}
124+
</Flexbox>
125+
);
126+
},
127+
);
128+
129+
export default ColorSwatches;

src/ColorSwatches/style.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { createStyles } from 'antd-style';
2+
3+
export const useStyles = createStyles(({ css, token, prefixCls }, size: number) => {
4+
return {
5+
active: css`
6+
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 20%);
7+
`,
8+
container: css`
9+
cursor: pointer;
10+
11+
flex: none;
12+
13+
width: ${size}px;
14+
min-width: ${size}px;
15+
height: ${size}px;
16+
min-height: ${size}px;
17+
18+
background: ${token.colorBgContainer};
19+
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 5%);
20+
21+
&:hover {
22+
box-shadow:
23+
inset 0 0 0 1px rgba(0, 0, 0, 5%),
24+
0 0 0 2px ${token.colorText};
25+
}
26+
`,
27+
picker: css`
28+
overflow: hidden;
29+
flex: none;
30+
31+
width: ${size}px;
32+
min-width: ${size}px;
33+
height: ${size}px;
34+
min-height: ${size}px;
35+
padding: 0;
36+
37+
border: none;
38+
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 5%);
39+
40+
&:hover {
41+
box-shadow:
42+
inset 0 0 0 1px rgba(0, 0, 0, 5%),
43+
0 0 0 2px ${token.colorText};
44+
}
45+
46+
.${prefixCls}-color-picker-color-block {
47+
width: 100%;
48+
height: 100%;
49+
border: none;
50+
border-radius: inherit;
51+
}
52+
`,
53+
};
54+
});

src/Swatches/demos/index.tsx

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/components.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export { default as Avatar, type AvatarProps } from './Avatar';
1010
export { default as Burger, type BurgerProps } from './Burger';
1111
export { default as CodeEditor, type CodeEditorProps } from './CodeEditor';
1212
export { default as Collapse, type CollapseProps } from './Collapse';
13+
export { default as ColorSwatches, type ColorSwatchesProps } from './ColorSwatches';
1314
export { type Config, default as ConfigProvider, useCdnFn } from './ConfigProvider';
1415
export { default as ContextMenu, type ContextMenuProps } from './ContextMenu';
1516
export { default as CopyButton, type CopyButtonProps } from './CopyButton';

0 commit comments

Comments
 (0)