diff --git a/package-lock.json b/package-lock.json index b487e17..d32e32a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@remix-run/serve": "^2.9.2", "@tanstack/react-query": "^5.51.23", "axios": "^1.7.2", + "cva": "npm:class-variance-authority@^0.7.0", "dayjs": "^1.11.11", "i18next": "^23.13.0", "i18next-browser-languagedetector": "^8.0.0", @@ -12435,6 +12436,26 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/cva": { + "name": "class-variance-authority", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.0.tgz", + "integrity": "sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==", + "dependencies": { + "clsx": "2.0.0" + }, + "funding": { + "url": "https://joebell.co.uk" + } + }, + "node_modules/cva/node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", diff --git a/package.json b/package.json index 941b6e3..8745fca 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@remix-run/serve": "^2.9.2", "@tanstack/react-query": "^5.51.23", "axios": "^1.7.2", + "cva": "npm:class-variance-authority@^0.7.0", "dayjs": "^1.11.11", "i18next": "^23.13.0", "i18next-browser-languagedetector": "^8.0.0", diff --git a/src/shared/ui/Button/Button.module.css b/src/shared/ui/Button/Button.module.css index 279ad96..d66da92 100644 --- a/src/shared/ui/Button/Button.module.css +++ b/src/shared/ui/Button/Button.module.css @@ -8,74 +8,84 @@ align-items: center; } -.Button:not(:disabled)[data-variant='filled'] { - &[data-color='primary'] { - background-color: var(--color-neutral-900); - color: var(--color-primary); - } - &[data-color='neutral'] { - background-color: var(--color-neutral-200); - color: #fff; - } -} -.Button:not(:disabled)[data-variant='outline'] { - &[data-color='primary'] { - border: 1px solid var(--color-primary); - color: var(--color-primary); - } - &[data-color='neutral'] { - border: 1px solid var(--color-neutral-300); - color: var(--color-neutral-900); - } -} -.Button:not(:disabled)[data-variant='ghost'] { +.Variant_Filled_Primary { + background-color: var(--color-neutral-900); + color: var(--color-primary); +} + +.Variant_Filled_Neutral { + background-color: var(--color-neutral-200); + color: #fff; +} + +.Variant_Outline_Primary { + border: 1px solid var(--color-primary); + color: var(--color-primary); +} + +.Variant_Outline_Neutral { + border: 1px solid var(--color-neutral-300); color: var(--color-neutral-900); - &[data-color='primary'] { - color: var(--color-primary); - } - &[data-color='neutral'] { - color: var(--color-neutral-900); - } } + +.Variant_Ghost { + +} + +.Variant_Ghost_Primary { + color: var(--color-primary); +} + +.Variant_Ghost_Neutral { + color: var(--color-neutral-900); +} + .Button:disabled { color: var(--color-neutral-300); background-color: var(--color-neutral-200); - - &[data-variant='ghost'] { - background-color: transparent; - } } +.Disabled_Ghost { + background-color: transparent !important; +} -.Button[data-size='M'] { +.Size_M { height: 48px; border-radius: 48px; padding: 8px 16px; font-size: 16px; font-weight: 600; } -.Button[data-size='S'] { + +.Size_S { height: 32px; border-radius: 40px; padding: 8px 12px; } -.Button[data-size='fit'] { + +.Size_Fit { padding-left: 0; padding-right: 0; } -.Button[data-width-type='fill'] { +.WidthType_Fill { width: 100%; } -.Button[data-width-type='hug'] { + +.WidthType_Hug { width: fit-content; } -.Button[data-text-align='left'] { +.TextAlign_Left { text-align: left; } -.Button[data-text-align='right'] { + +.TextAlign_Center { + text-align: center; +} + +.TextAlign_Right { text-align: right; } diff --git a/src/shared/ui/Button/Button.stories.tsx b/src/shared/ui/Button/Button.stories.tsx index 53a37d7..129eec1 100644 --- a/src/shared/ui/Button/Button.stories.tsx +++ b/src/shared/ui/Button/Button.stories.tsx @@ -9,6 +9,51 @@ const meta: Meta = { export default meta; type Story = StoryObj; +export const Default: Story = { + render: () => { + const variantList = ['filled', 'outline', 'ghost'] as const; + const colorList = ['primary', 'neutral'] as const; + const widthType = ['fill', 'hug'] as const; + const size = ['fit', 'S', 'M'] as const; + return ( +
+
+

variant

+ {variantList.map((v) => ( + + ))} +
+
+

color

+ {colorList.map((v) => ( + + ))} +
+
+

widthType

+ {widthType.map((v) => ( + + ))} +
+
+

size

+ {size.map((v) => ( + + ))} +
+
+ ); + }, +}; + export const PrimaryButton: Story = { args: { variant: 'filled', diff --git a/src/shared/ui/Button/Button.tsx b/src/shared/ui/Button/Button.tsx index 65c4ba4..ce5bfad 100644 --- a/src/shared/ui/Button/Button.tsx +++ b/src/shared/ui/Button/Button.tsx @@ -1,37 +1,49 @@ import { ButtonHTMLAttributes, DetailedHTMLProps, ReactNode } from 'react'; import styles from './Button.module.css'; +import { cva, VariantProps } from 'cva'; + +const buttonStyle = cva(styles.Button, { + variants: { + widthType: { fill: styles.WidthType_Fill, hug: styles.WidthType_Hug }, + textAlign: { left: styles.TextAlign_Left, center: styles.TextAlign_Center, right: styles.TextAlign_Right }, + size: { M: styles.Size_M, S: styles.Size_S, fit: styles.Size_Fit }, + variant: { filled: '', outline: '', ghost: '' }, + color: { primary: '', neutral: '' }, + }, + compoundVariants: [ + { variant: 'filled', color: 'primary', className: styles.Variant_Filled_Primary }, + { variant: 'filled', color: 'neutral', className: styles.Variant_Filled_Neutral }, + { variant: 'outline', color: 'primary', className: styles.Variant_Outline_Primary }, + { variant: 'outline', color: 'neutral', className: styles.Variant_Outline_Neutral }, + { variant: 'ghost', color: 'primary', className: styles.Variant_Ghost_Primary }, + { variant: 'ghost', color: 'neutral', className: styles.Variant_Ghost_Neutral }, + ], + defaultVariants: { + variant: 'filled', + color: 'primary', + size: 'M', + textAlign: 'center', + }, +}); type ButtonProps = DetailedHTMLProps, HTMLButtonElement> & { - variant: 'filled' | 'outline' | 'ghost' | 'link'; - color: 'primary' | 'neutral'; - widthType: 'fill' | 'hug'; suffixSlot?: ReactNode; prefixSlot?: ReactNode; - textAlign?: 'left' | 'center' | 'right'; - size?: 'fit' | 'S' | 'M'; -}; +} & VariantProps; export const Button = ({ className = '', variant, color, - size = 'M', - widthType = 'fill', - textAlign = 'center', + size, + widthType, + textAlign, suffixSlot, prefixSlot, children, ...props }: ButtonProps) => ( -