Skip to content

Commit 5f6016c

Browse files
committed
feat: add component checkbox
1 parent ed65a6b commit 5f6016c

File tree

10 files changed

+315
-0
lines changed

10 files changed

+315
-0
lines changed

packages/ui-variants/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export * from './variants/button';
1212
export * from './variants/button-group';
1313
export * from './variants/card';
1414
export * from './variants/carousel';
15+
export * from './variants/checkbox';
1516
export * from './variants/divider';
1617
export * from './variants/label';
1718
export * from './variants/scroll-area';
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { tv } from 'tailwind-variants';
2+
3+
export const checkboxVariants = tv({
4+
defaultVariants: {
5+
color: 'primary',
6+
orientation: 'horizontal',
7+
size: 'md'
8+
},
9+
slots: {
10+
control: [
11+
'peer shrink-0 rounded-sm border shadow',
12+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-background ',
13+
'disabled:cursor-not-allowed disabled:opacity-50'
14+
],
15+
groupRoot: 'flex',
16+
indicator: 'size-full flex items-center justify-center text-current',
17+
label: '',
18+
root: 'flex items-center'
19+
},
20+
variants: {
21+
color: {
22+
accent: {
23+
control: `border-accent-foreground/50 focus-visible:ring-accent-foreground/20 data-[state=checked]:bg-accent-foreground/5 data-[state=checked]:text-accent-foreground data-[state=indeterminate]:bg-accent-foreground/5 data-[state=indeterminate]:text-accent-foreground`
24+
},
25+
carbon: {
26+
control: `border-carbon focus-visible:ring-carbon data-[state=checked]:bg-carbon data-[state=checked]:text-carbon-foreground data-[state=indeterminate]:bg-carbon data-[state=indeterminate]:text-carbon-foreground`
27+
},
28+
destructive: {
29+
control: `border-destructive focus-visible:ring-destructive data-[state=checked]:bg-destructive data-[state=checked]:text-destructive-foreground data-[state=indeterminate]:bg-destructive data-[state=indeterminate]:text-destructive-foreground`
30+
},
31+
info: {
32+
control: `border-info focus-visible:ring-info data-[state=checked]:bg-info data-[state=checked]:text-info-foreground data-[state=indeterminate]:bg-info data-[state=indeterminate]:text-info-foreground`
33+
},
34+
primary: {
35+
control: `border-primary focus-visible:ring-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground`
36+
},
37+
secondary: {
38+
control: `border-secondary-foreground/50 focus-visible:ring-secondary-foreground/20 data-[state=checked]:bg-secondary-foreground/5 data-[state=checked]:text-secondary-foreground data-[state=indeterminate]:bg-secondary-foreground/5 data-[state=indeterminate]:text-secondary-foreground`
39+
},
40+
success: {
41+
control: `border-success focus-visible:ring-success data-[state=checked]:bg-success data-[state=checked]:text-success-foreground data-[state=indeterminate]:bg-success data-[state=indeterminate]:text-success-foreground`
42+
},
43+
warning: {
44+
control: `border-warning focus-visible:ring-warning data-[state=checked]:bg-warning data-[state=checked]:text-warning-foreground data-[state=indeterminate]:bg-warning data-[state=indeterminate]:text-warning-foreground`
45+
}
46+
},
47+
orientation: {
48+
horizontal: {
49+
groupRoot: 'items-center'
50+
},
51+
vertical: {
52+
groupRoot: 'flex-col'
53+
}
54+
},
55+
size: {
56+
'2xl': {
57+
control: 'size-6',
58+
groupRoot: 'gap-x-4.5 gap-y-3.5',
59+
root: 'gap-3.5'
60+
},
61+
lg: {
62+
control: 'size-4.5',
63+
groupRoot: 'gap-x-3.5 gap-y-2.5',
64+
root: 'gap-2.5'
65+
},
66+
md: {
67+
control: 'size-4',
68+
groupRoot: 'gap-x-3 gap-y-2',
69+
root: 'gap-2'
70+
},
71+
sm: {
72+
control: 'size-3.5',
73+
groupRoot: 'gap-x-2.5 gap-y-1.75',
74+
root: 'gap-1.75'
75+
},
76+
xl: {
77+
control: 'size-5',
78+
groupRoot: 'gap-x-4 gap-y-3',
79+
root: 'gap-3'
80+
},
81+
xs: {
82+
control: 'size-3',
83+
groupRoot: 'gap-x-2 gap-y-1.5',
84+
root: 'gap-1.5'
85+
}
86+
}
87+
}
88+
});
89+
90+
export type CheckboxSlots = keyof typeof checkboxVariants.slots;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export { default as Checkbox } from './source/Checkbox';
2+
export { default as CheckboxControl } from './source/CheckboxControl';
3+
export { default as CheckboxIndicator } from './source/CheckboxIndicator';
4+
export { default as CheckboxRoot } from './source/CheckboxRoot';
5+
6+
export * from './types';
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Check, Minus } from 'lucide-react';
2+
import React, { forwardRef } from 'react';
3+
4+
import CheckboxLabel from '../../label/source/Label';
5+
import type { CheckboxProps } from '../types';
6+
7+
import CheckboxControl from './CheckboxControl';
8+
import CheckboxIndicator from './CheckboxIndicator';
9+
import CheckboxRoot from './CheckboxRoot';
10+
11+
const Checkbox = forwardRef<HTMLDivElement, CheckboxProps>((props, ref) => {
12+
const { children, className, classNames, forceMountIndicator, size, ...rest } = props;
13+
14+
const isIndeterminate = rest.checked === 'indeterminate';
15+
16+
return (
17+
<CheckboxRoot
18+
className={className || classNames?.root}
19+
ref={ref}
20+
>
21+
<CheckboxControl
22+
className={classNames?.control}
23+
size={size}
24+
{...rest}
25+
>
26+
<CheckboxIndicator
27+
className={classNames?.indicator}
28+
forceMount={forceMountIndicator}
29+
>
30+
{isIndeterminate ? <Minus className="size-full" /> : <Check className="size-full" />}
31+
</CheckboxIndicator>
32+
</CheckboxControl>
33+
34+
{children && (
35+
<CheckboxLabel
36+
className={classNames?.label}
37+
htmlFor={rest.id}
38+
>
39+
{children}
40+
</CheckboxLabel>
41+
)}
42+
</CheckboxRoot>
43+
);
44+
});
45+
46+
Checkbox.displayName = 'Checkbox';
47+
48+
export default Checkbox;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Root } from '@radix-ui/react-checkbox';
2+
import { checkboxVariants, cn } from '@soybean-react-ui/variants';
3+
import React from 'react';
4+
5+
import type { CheckboxControlProps } from '../types';
6+
7+
const CheckboxControl = React.forwardRef<React.ComponentRef<typeof Root>, CheckboxControlProps>((props, ref) => {
8+
const { className, color, size, ...rest } = props;
9+
10+
const { control } = checkboxVariants({ color, size });
11+
12+
const mergedCls = cn(control(), className);
13+
14+
return (
15+
<Root
16+
className={mergedCls}
17+
{...rest}
18+
ref={ref}
19+
/>
20+
);
21+
});
22+
23+
CheckboxControl.displayName = 'CheckboxControl';
24+
25+
export default CheckboxControl;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Indicator } from '@radix-ui/react-checkbox';
2+
import { checkboxVariants, cn } from '@soybean-react-ui/variants';
3+
import React from 'react';
4+
5+
import type { CheckboxIndicatorProps } from '../types';
6+
7+
const CheckboxIndicator = React.forwardRef<React.ComponentRef<typeof Indicator>, CheckboxIndicatorProps>(
8+
(props, ref) => {
9+
const { className, ...rest } = props;
10+
11+
const { indicator } = checkboxVariants();
12+
13+
const mergedCls = cn(indicator(), className);
14+
15+
return (
16+
<Indicator
17+
className={mergedCls}
18+
{...rest}
19+
ref={ref}
20+
/>
21+
);
22+
}
23+
);
24+
25+
CheckboxIndicator.displayName = 'CheckboxIndicator';
26+
27+
export default CheckboxIndicator;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { checkboxVariants, cn } from '@soybean-react-ui/variants';
2+
import React from 'react';
3+
4+
import type { CheckboxRootProps } from '../types';
5+
6+
const CheckboxRoot = React.forwardRef<HTMLDivElement, CheckboxRootProps>((props, ref) => {
7+
const { className, size, ...rest } = props;
8+
9+
const { root } = checkboxVariants({ size });
10+
11+
const mergedCls = cn(root(), className);
12+
13+
return (
14+
<div
15+
className={mergedCls}
16+
{...rest}
17+
ref={ref}
18+
/>
19+
);
20+
});
21+
22+
CheckboxRoot.displayName = 'CheckboxRoot';
23+
24+
export default CheckboxRoot;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type {
2+
CheckboxIndicatorProps as _CheckboxIndicatorProps,
3+
CheckboxProps as _CheckboxRootProps
4+
} from '@radix-ui/react-checkbox';
5+
import type { CheckboxSlots } from '@soybean-react-ui/variants';
6+
7+
import type { BaseComponentProps, BaseNodeProps, ClassValue, ThemeColor } from '../../types';
8+
9+
export type CheckboxUi = Partial<Record<CheckboxSlots, ClassValue>>;
10+
11+
export type CheckboxControlProps = BaseNodeProps<_CheckboxRootProps> & {
12+
color?: ThemeColor;
13+
};
14+
15+
export type CheckboxIndicatorProps = BaseNodeProps<_CheckboxIndicatorProps>;
16+
17+
export type CheckboxRootProps = BaseComponentProps<'div'>;
18+
19+
export type CheckboxProps = CheckboxControlProps & {
20+
classNames?: CheckboxUi;
21+
forceMountIndicator?: true;
22+
};

packages/ui/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export * from './components/card';
1818

1919
export * from './components/carousel';
2020

21+
export * from './components/checkbox';
22+
2123
export * from './components/divider';
2224

2325
export * from './components/label';
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import type { ThemeColor, ThemeSize } from 'soybean-react-ui';
2+
import { Card, Checkbox } from 'soybean-react-ui';
3+
4+
const colors: ThemeColor[] = ['primary', 'destructive', 'success', 'warning', 'info', 'carbon', 'secondary', 'accent'];
5+
6+
const sizes: ThemeSize[] = ['xs', 'sm', 'md', 'lg', 'xl', '2xl'];
7+
8+
const items = [
9+
{ label: 'A', value: '1' },
10+
{ label: 'B', value: '2' },
11+
{ label: 'C', value: '3' }
12+
];
13+
14+
const CheckboxPage = () => {
15+
return (
16+
<div className="flex-c gap-4">
17+
<Card
18+
split
19+
title="Color"
20+
>
21+
<div className="flex flex-wrap gap-[12px]">
22+
{colors.map(color => (
23+
<Checkbox
24+
color={color}
25+
id={color}
26+
key={color}
27+
>
28+
{color}
29+
</Checkbox>
30+
))}
31+
</div>
32+
</Card>
33+
34+
<Card
35+
split
36+
title="Size"
37+
>
38+
<div className="flex flex-wrap gap-[12px]">
39+
{sizes.map(size => (
40+
<Checkbox
41+
checked="indeterminate"
42+
key={size}
43+
size={size}
44+
>
45+
{size}
46+
</Checkbox>
47+
))}
48+
</div>
49+
</Card>
50+
51+
<Card
52+
split
53+
title="Disabled"
54+
>
55+
{items.map(item => (
56+
<Checkbox
57+
disabled
58+
defaultChecked={item.value === '2'}
59+
key={item.value}
60+
value={item.value}
61+
>
62+
{item.label}
63+
</Checkbox>
64+
))}
65+
</Card>
66+
</div>
67+
);
68+
};
69+
70+
export default CheckboxPage;

0 commit comments

Comments
 (0)