From 8efad746e373eee27d17eaebdb2b6fafa0298e1c Mon Sep 17 00:00:00 2001 From: osohyun0224 <53892427+osohyun0224@users.noreply.github.com> Date: Tue, 28 Apr 2026 18:24:09 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(button):=205=EC=84=B8=EB=8C=80=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EB=B2=84=ED=8A=BC=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=A0=84=EB=A9=B4=20=EA=B0=9C=ED=8E=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/button/src/Button.css.ts | 93 +++++++++++----- packages/button/src/Button.stories.tsx | 146 +++++++++++++++++++++++-- packages/button/src/Button.test.tsx | 86 +++++++-------- packages/button/src/Button.tsx | 26 ++++- packages/tokens/src/colors/colors.ts | 9 ++ tokens/primitive/color.json | 20 ++-- tokens/primitive/radius.json | 8 +- tokens/primitive/spacing.json | 4 +- tokens/primitive/typography.json | 6 +- 9 files changed, 288 insertions(+), 110 deletions(-) diff --git a/packages/button/src/Button.css.ts b/packages/button/src/Button.css.ts index b0f73c5..ca3f10a 100644 --- a/packages/button/src/Button.css.ts +++ b/packages/button/src/Button.css.ts @@ -1,78 +1,115 @@ -import { vars } from '@sipe-team/tokens'; +import { color, vars } from '@sipe-team/tokens'; -import { style } from '@vanilla-extract/css'; +import { createVar, style } from '@vanilla-extract/css'; import { recipe } from '@vanilla-extract/recipes'; import { ButtonSize, ButtonVariant } from './Button'; -export const disabled = style({ - opacity: 0.4, - cursor: 'not-allowed', - pointerEvents: 'none', +export const buttonOrange = createVar(); +export const buttonGradient = createVar(); +export const buttonRed = createVar(); + +export const iconWrapper = style({ + display: 'flex', + alignItems: 'center', + flexShrink: 0, +}); + +export const iconLayout = style({ + gap: vars.spacing.md, }); export const button = recipe({ base: { + vars: { + [buttonOrange]: '#FF7C27', + [buttonGradient]: 'linear-gradient(225deg, #FF4500 0%, #FFB24D 100%)', + [buttonRed]: '#FE4E07', + }, display: 'flex', alignItems: 'center', justifyContent: 'center', - borderRadius: vars.radius.md, + fontStyle: 'normal', fontWeight: vars.typography.fontWeight.semiBold, + lineHeight: '150%', cursor: 'pointer', transition: 'all 0.2s ease-in-out', border: 'none', fontFamily: vars.typography.fontFamily, ':focus-visible': { - outline: `2px solid ${vars.color.primary}`, + outline: `2px solid ${buttonOrange}`, outlineOffset: '2px', }, + selectors: { + '&:disabled, &[aria-disabled="true"]': { + backgroundColor: color.gray500, + color: color.gray600, + cursor: 'not-allowed', + pointerEvents: 'none', + border: 'none', + background: color.gray500, + }, + }, }, variants: { variant: { - [ButtonVariant.filled]: { - backgroundColor: vars.color.primary, - color: vars.color.background, - border: 'none', + [ButtonVariant.fill]: { + backgroundColor: buttonOrange, + color: '#000', ':hover': { - opacity: 0.9, + background: buttonGradient, + }, + ':active': { + background: buttonRed, }, }, [ButtonVariant.outline]: { backgroundColor: 'transparent', - border: `1px solid ${vars.color.primary}`, - color: vars.color.primary, - ':hover': { - backgroundColor: vars.color.primary, - color: vars.color.background, - }, + border: `1px solid ${buttonOrange}`, + color: buttonOrange, }, [ButtonVariant.ghost]: { backgroundColor: 'transparent', border: 'none', - color: vars.color.primary, + color: buttonOrange, ':hover': { - backgroundColor: `color-mix(in srgb, ${vars.color.primary} 10%, transparent)`, + opacity: 0.8, + }, + ':active': { + color: buttonRed, }, }, }, size: { [ButtonSize.sm]: { height: '32px', - padding: `0 ${vars.spacing.sm}`, - fontSize: vars.typography.fontSize['200'], - lineHeight: vars.typography.lineHeight.compact, + padding: `0 ${vars.spacing.xs}`, + borderRadius: vars.radius.md, + fontSize: vars.typography.fontSize['050'], + }, + [ButtonSize.md]: { + height: '40px', + padding: '0 12px', + borderRadius: '6px', + fontSize: vars.typography.fontSize['100'], }, [ButtonSize.lg]: { height: '48px', - padding: `0 ${vars.spacing.lg}`, - fontSize: vars.typography.fontSize['400'], - lineHeight: vars.typography.lineHeight.regular, + padding: `0 ${vars.spacing.sm}`, + borderRadius: '6px', + fontSize: vars.typography.fontSize['200'], + }, + [ButtonSize.xl]: { + height: '64px', + padding: `0 ${vars.spacing.md}`, + borderRadius: vars.radius.lg, + fontSize: vars.typography.fontSize['500'], }, }, }, compoundVariants: [], defaultVariants: { - variant: ButtonVariant.filled, + variant: ButtonVariant.fill, size: ButtonSize.lg, }, }); diff --git a/packages/button/src/Button.stories.tsx b/packages/button/src/Button.stories.tsx index 3c64878..5df9949 100644 --- a/packages/button/src/Button.stories.tsx +++ b/packages/button/src/Button.stories.tsx @@ -2,6 +2,70 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Button } from './Button'; +const ArrowRightIcon = () => ( + + + + +); + +const SearchIcon = () => ( + + + + +); + +const DownloadIcon = () => ( + + + + + +); + +const ChevronRightIcon = () => ( + + + +); + const meta = { title: 'Components/Button', component: Button, @@ -11,12 +75,12 @@ const meta = { argTypes: { variant: { description: 'The visual style of the button', - options: ['filled', 'outline', 'ghost'], + options: ['fill', 'outline', 'ghost'], control: { type: 'radio' }, }, size: { description: 'The size of the button', - options: ['sm', 'lg'], + options: ['sm', 'md', 'lg', 'xl'], control: { type: 'radio' }, }, disabled: { @@ -32,7 +96,7 @@ type Story = StoryObj; export const Basic: Story = { args: { children: 'Button', - variant: 'filled', + variant: 'fill', size: 'lg', }, }; @@ -42,9 +106,9 @@ export const Variants: Story = { children: 'Button', }, render: (args) => ( -
- + +
), }; -export const States: Story = { +export const Disabled: Story = { args: { children: 'Button', }, render: (args) => ( -
- - + +
), }; + +export const WithLeftIcon: Story = { + args: { + children: 'Search', + variant: 'fill', + size: 'xl', + leftIcon: , + }, +}; + +export const WithRightIcon: Story = { + args: { + children: 'Next', + variant: 'fill', + size: 'xl', + rightIcon: , + }, +}; + +export const WithBothIcons: Story = { + args: { + children: 'Download', + variant: 'fill', + size: 'xl', + leftIcon: , + rightIcon: , + }, +}; + +export const AllVariantsAndSizes: Story = { + render: () => ( +
+ {(['fill', 'outline', 'ghost'] as const).map((variant) => ( +
+
{variant}
+
+ {(['sm', 'md', 'lg', 'xl'] as const).map((size) => ( + + ))} +
+
+ ))} +
+ ), +}; diff --git a/packages/button/src/Button.test.tsx b/packages/button/src/Button.test.tsx index 7530a64..f0e9f13 100644 --- a/packages/button/src/Button.test.tsx +++ b/packages/button/src/Button.test.tsx @@ -5,94 +5,90 @@ import { Button } from './Button'; test('displays text passed as children', () => { render(); - expect(screen.getByText('Test')).toBeInTheDocument(); }); test('applies correct classes', () => { render(); - const button = screen.getByRole('button'); - - // Should have button classes applied expect(button.className).toBeTruthy(); expect(button.className.length).toBeGreaterThan(0); }); -test('uses filled variant as default when variant is not provided', () => { +test('uses fill variant as default when variant is not provided', () => { render(); - const button = screen.getByRole('button'); - - // Should render properly expect(button).toBeInTheDocument(); expect(button.className).toBeTruthy(); }); -test('size prop works correctly', () => { +test('size sm works correctly', () => { render(); + expect(screen.getByRole('button')).toBeInTheDocument(); +}); - const button = screen.getByRole('button'); +test('size md works correctly', () => { + render(); + expect(screen.getByRole('button')).toBeInTheDocument(); +}); - // Should render without errors - expect(button).toBeInTheDocument(); - expect(button.className).toBeTruthy(); +test('size lg works correctly', () => { + render(); + expect(screen.getByRole('button')).toBeInTheDocument(); }); -test('ghost variant works correctly', () => { - render(); +test('size xl works correctly', () => { + render(); + expect(screen.getByRole('button')).toBeInTheDocument(); +}); - const button = screen.getByRole('button'); +test('fill variant works correctly', () => { + render(); + expect(screen.getByRole('button')).toBeInTheDocument(); +}); - // Should render without errors - expect(button).toBeInTheDocument(); - expect(button.className).toBeTruthy(); +test('ghost variant works correctly', () => { + render(); + expect(screen.getByRole('button')).toBeInTheDocument(); }); test('outline variant works correctly', () => { render(); - - const button = screen.getByRole('button'); - - // Should render without errors - expect(button).toBeInTheDocument(); - expect(button.className).toBeTruthy(); + expect(screen.getByRole('button')).toBeInTheDocument(); }); test('disabled state works correctly', () => { render(); - const button = screen.getByRole('button'); - - // Should be disabled expect(button).toBeDisabled(); - expect(button).toBeInTheDocument(); }); -test('large size works correctly', () => { - render(); +test('renders left icon', () => { + render(); + expect(screen.getByTestId('left-icon')).toBeInTheDocument(); +}); - const button = screen.getByRole('button'); +test('renders right icon', () => { + render(); + expect(screen.getByTestId('right-icon')).toBeInTheDocument(); +}); - // Should render without errors - expect(button).toBeInTheDocument(); - expect(button.className).toBeTruthy(); +test('renders both icons', () => { + render( + , + ); + expect(screen.getByTestId('left-icon')).toBeInTheDocument(); + expect(screen.getByTestId('right-icon')).toBeInTheDocument(); }); test('type="button" is used as default when type is not provided', () => { render(); - - const button = screen.getByRole('button'); - - // Should default to type="button" - expect(button).toHaveAttribute('type', 'button'); + expect(screen.getByRole('button')).toHaveAttribute('type', 'button'); }); test('type="submit" works correctly', () => { render(); - - const button = screen.getByRole('button'); - - // Should allow overriding type - expect(button).toHaveAttribute('type', 'submit'); + expect(screen.getByRole('button')).toHaveAttribute('type', 'submit'); }); diff --git a/packages/button/src/Button.tsx b/packages/button/src/Button.tsx index 98a343a..33c13ff 100644 --- a/packages/button/src/Button.tsx +++ b/packages/button/src/Button.tsx @@ -1,4 +1,4 @@ -import { type ComponentProps, type ForwardedRef, forwardRef } from 'react'; +import { type ComponentProps, type ForwardedRef, forwardRef, type ReactNode } from 'react'; import { Slot } from '@radix-ui/react-slot'; @@ -7,7 +7,7 @@ import { clsx as cx } from 'clsx'; import * as styles from './Button.css'; export const ButtonVariant = { - filled: 'filled', + fill: 'fill', outline: 'outline', ghost: 'ghost', } as const; @@ -15,30 +15,44 @@ export type ButtonVariant = (typeof ButtonVariant)[keyof typeof ButtonVariant]; export const ButtonSize = { sm: 'sm', + md: 'md', lg: 'lg', + xl: 'xl', } as const; export type ButtonSize = (typeof ButtonSize)[keyof typeof ButtonSize]; export interface ButtonProps extends ComponentProps<'button'> { variant?: ButtonVariant; size?: ButtonSize; + leftIcon?: ReactNode; + rightIcon?: ReactNode; asChild?: boolean; } export const Button = forwardRef(function Button( { - variant = ButtonVariant.filled, + variant = ButtonVariant.fill, size = ButtonSize.lg, type = 'button', + leftIcon, + rightIcon, asChild, disabled, className: _className, + children, ...props }: ButtonProps, ref: ForwardedRef, ) { const Comp = asChild ? Slot : 'button'; - const className = cx(styles.button({ variant, size }), { [styles.disabled]: disabled }, _className); - - return ; + const hasIcon = !!(leftIcon || rightIcon); + const className = cx(styles.button({ variant, size }), hasIcon && styles.iconLayout, _className); + + return ( + + {leftIcon && {leftIcon}} + {children} + {rightIcon && {rightIcon}} + + ); }); diff --git a/packages/tokens/src/colors/colors.ts b/packages/tokens/src/colors/colors.ts index 782beec..7377a5a 100644 --- a/packages/tokens/src/colors/colors.ts +++ b/packages/tokens/src/colors/colors.ts @@ -171,9 +171,18 @@ export const theme4th: ThemeColor = { gradient: 'linear-gradient(45deg, #FF9595 0%, #FFE5B1 100%)', }; +export const theme5th: ThemeColor = { + primary: '#FF7C27', + secondary: '#FE4E07', + background: '#000000', + text: '#000000', + gradient: 'linear-gradient(225deg, #FF4500 0%, #FFB24D 100%)', +}; + export const themeColor = { '1st': theme1st, '2nd': theme2nd, '3rd': theme3rd, '4th': theme4th, + '5th': theme5th, } as const; diff --git a/tokens/primitive/color.json b/tokens/primitive/color.json index 7bc9490..efb46a4 100644 --- a/tokens/primitive/color.json +++ b/tokens/primitive/color.json @@ -3,7 +3,7 @@ "black": { "value": "#131518", "type": "color" }, "white": { "value": "#ffffff", "type": "color" }, "gray": { - "50": { "value": "#fafafa", "type": "color" }, + "50": { "value": "#fafafa", "type": "color" }, "100": { "value": "#f4f4f5", "type": "color" }, "200": { "value": "#e4e4e7", "type": "color" }, "300": { "value": "#d4d4d8", "type": "color" }, @@ -16,7 +16,7 @@ "950": { "value": "#111111", "type": "color" } }, "red": { - "50": { "value": "#fef2f2", "type": "color" }, + "50": { "value": "#fef2f2", "type": "color" }, "100": { "value": "#fee2e2", "type": "color" }, "200": { "value": "#fecaca", "type": "color" }, "300": { "value": "#fca5a5", "type": "color" }, @@ -29,7 +29,7 @@ "950": { "value": "#1f0808", "type": "color" } }, "orange": { - "50": { "value": "#fff7ed", "type": "color" }, + "50": { "value": "#fff7ed", "type": "color" }, "100": { "value": "#ffedd5", "type": "color" }, "200": { "value": "#fed7aa", "type": "color" }, "300": { "value": "#fdba74", "type": "color" }, @@ -42,7 +42,7 @@ "950": { "value": "#220a04", "type": "color" } }, "yellow": { - "50": { "value": "#fefce8", "type": "color" }, + "50": { "value": "#fefce8", "type": "color" }, "100": { "value": "#fef9c3", "type": "color" }, "200": { "value": "#fef08a", "type": "color" }, "300": { "value": "#fde047", "type": "color" }, @@ -55,7 +55,7 @@ "950": { "value": "#281304", "type": "color" } }, "green": { - "50": { "value": "#f0fdf4", "type": "color" }, + "50": { "value": "#f0fdf4", "type": "color" }, "100": { "value": "#dcfce7", "type": "color" }, "200": { "value": "#bbf7d0", "type": "color" }, "300": { "value": "#86efac", "type": "color" }, @@ -68,7 +68,7 @@ "950": { "value": "#03190c", "type": "color" } }, "teal": { - "50": { "value": "#f0fdfa", "type": "color" }, + "50": { "value": "#f0fdfa", "type": "color" }, "100": { "value": "#ccfbf1", "type": "color" }, "200": { "value": "#99f6e4", "type": "color" }, "300": { "value": "#5eead4", "type": "color" }, @@ -81,7 +81,7 @@ "950": { "value": "#021716", "type": "color" } }, "blue": { - "50": { "value": "#eff6ff", "type": "color" }, + "50": { "value": "#eff6ff", "type": "color" }, "100": { "value": "#dbeafe", "type": "color" }, "200": { "value": "#bfdbfe", "type": "color" }, "300": { "value": "#a3cfff", "type": "color" }, @@ -94,7 +94,7 @@ "950": { "value": "#0c142e", "type": "color" } }, "cyan": { - "50": { "value": "#ecfeff", "type": "color" }, + "50": { "value": "#ecfeff", "type": "color" }, "100": { "value": "#cffafe", "type": "color" }, "200": { "value": "#a5f3fc", "type": "color" }, "300": { "value": "#00ffff", "type": "color" }, @@ -107,7 +107,7 @@ "950": { "value": "#051b24", "type": "color" } }, "purple": { - "50": { "value": "#faf5ff", "type": "color" }, + "50": { "value": "#faf5ff", "type": "color" }, "100": { "value": "#f3e8ff", "type": "color" }, "200": { "value": "#e9d5ff", "type": "color" }, "300": { "value": "#d8b4fe", "type": "color" }, @@ -120,7 +120,7 @@ "950": { "value": "#1a032e", "type": "color" } }, "pink": { - "50": { "value": "#fdf2f8", "type": "color" }, + "50": { "value": "#fdf2f8", "type": "color" }, "100": { "value": "#fce7f3", "type": "color" }, "200": { "value": "#fbcfe8", "type": "color" }, "300": { "value": "#f9a8d4", "type": "color" }, diff --git a/tokens/primitive/radius.json b/tokens/primitive/radius.json index b2623a7..0d728b8 100644 --- a/tokens/primitive/radius.json +++ b/tokens/primitive/radius.json @@ -1,9 +1,9 @@ { "radius": { - "2": { "value": "2px", "type": "dimension" }, - "4": { "value": "4px", "type": "dimension" }, - "8": { "value": "8px", "type": "dimension" }, - "12": { "value": "12px", "type": "dimension" }, + "2": { "value": "2px", "type": "dimension" }, + "4": { "value": "4px", "type": "dimension" }, + "8": { "value": "8px", "type": "dimension" }, + "12": { "value": "12px", "type": "dimension" }, "full": { "value": "9999px", "type": "dimension" } } } diff --git a/tokens/primitive/spacing.json b/tokens/primitive/spacing.json index 07425b4..f67ac4f 100644 --- a/tokens/primitive/spacing.json +++ b/tokens/primitive/spacing.json @@ -1,7 +1,7 @@ { "spacing": { - "4": { "value": "4px", "type": "dimension" }, - "8": { "value": "8px", "type": "dimension" }, + "4": { "value": "4px", "type": "dimension" }, + "8": { "value": "8px", "type": "dimension" }, "12": { "value": "12px", "type": "dimension" }, "16": { "value": "16px", "type": "dimension" }, "20": { "value": "20px", "type": "dimension" }, diff --git a/tokens/primitive/typography.json b/tokens/primitive/typography.json index 88039b0..dbb6f68 100644 --- a/tokens/primitive/typography.json +++ b/tokens/primitive/typography.json @@ -16,10 +16,10 @@ "48": { "value": "48px", "type": "dimension" } }, "fontWeight": { - "regular": { "value": "400", "type": "fontWeight" }, - "medium": { "value": "500", "type": "fontWeight" }, + "regular": { "value": "400", "type": "fontWeight" }, + "medium": { "value": "500", "type": "fontWeight" }, "semiBold": { "value": "600", "type": "fontWeight" }, - "bold": { "value": "700", "type": "fontWeight" } + "bold": { "value": "700", "type": "fontWeight" } }, "lineHeight": { "compact": { "value": "1.3", "type": "number" }, From 90019aafe870ea9f9ce5da9b561a5f77cc1e746c Mon Sep 17 00:00:00 2001 From: osohyun0224 <53892427+osohyun0224@users.noreply.github.com> Date: Tue, 28 Apr 2026 18:27:21 +0900 Subject: [PATCH 2/4] feat(button): add to changeset docs --- .changeset/button-v2-redesign.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .changeset/button-v2-redesign.md diff --git a/.changeset/button-v2-redesign.md b/.changeset/button-v2-redesign.md new file mode 100644 index 0000000..38ddb03 --- /dev/null +++ b/.changeset/button-v2-redesign.md @@ -0,0 +1,14 @@ +--- +"@sipe-team/button": major +"@sipe-team/tokens": minor +--- + +Redesign Button component based on 5th generation design system + +- **BREAKING**: Rename `ButtonVariant.filled` to `ButtonVariant.fill` +- **BREAKING**: Expand `ButtonSize` from `sm | lg` to `sm | md | lg | xl` +- Add `leftIcon` and `rightIcon` props for icon support +- Apply 5th design colors via `createVar()` (button-scoped CSS variables) +- Add interaction states: hover (gradient), pressed (`#FE4E07`), disabled (`gray500/600`) +- Fix disabled CSS specificity bug by moving styles into recipe base selectors +- Add `theme5th` color token to `@sipe-team/tokens` From d95c95338945a09e6158387a830900dee0ba8d6d Mon Sep 17 00:00:00 2001 From: osohyun0224 <53892427+osohyun0224@users.noreply.github.com> Date: Wed, 29 Apr 2026 18:28:56 +0900 Subject: [PATCH 3/4] fix(Button): use Slottable for multi-child asChild compatibility --- packages/button/src/Button.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/button/src/Button.tsx b/packages/button/src/Button.tsx index 33c13ff..559a3d1 100644 --- a/packages/button/src/Button.tsx +++ b/packages/button/src/Button.tsx @@ -1,6 +1,6 @@ import { type ComponentProps, type ForwardedRef, forwardRef, type ReactNode } from 'react'; -import { Slot } from '@radix-ui/react-slot'; +import { Slot, Slottable } from '@radix-ui/react-slot'; import { clsx as cx } from 'clsx'; @@ -51,7 +51,7 @@ export const Button = forwardRef(function Button( return ( {leftIcon && {leftIcon}} - {children} + {children} {rightIcon && {rightIcon}} ); From 831d8afe85adf9c63effb401c46fb2826acaa6ae Mon Sep 17 00:00:00 2001 From: osohyun0224 <53892427+osohyun0224@users.noreply.github.com> Date: Wed, 29 Apr 2026 18:42:41 +0900 Subject: [PATCH 4/4] fix(Button): apply to typecheck CI --- packages/button/src/Button.css.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/button/src/Button.css.ts b/packages/button/src/Button.css.ts index ca3f10a..dbe0b4d 100644 --- a/packages/button/src/Button.css.ts +++ b/packages/button/src/Button.css.ts @@ -16,7 +16,7 @@ export const iconWrapper = style({ }); export const iconLayout = style({ - gap: vars.spacing.md, + gap: vars.spacing.component.md, }); export const button = recipe({ @@ -83,8 +83,8 @@ export const button = recipe({ size: { [ButtonSize.sm]: { height: '32px', - padding: `0 ${vars.spacing.xs}`, - borderRadius: vars.radius.md, + padding: `0 ${vars.spacing.component.xs}`, + borderRadius: vars.radius.component.md, fontSize: vars.typography.fontSize['050'], }, [ButtonSize.md]: { @@ -95,14 +95,14 @@ export const button = recipe({ }, [ButtonSize.lg]: { height: '48px', - padding: `0 ${vars.spacing.sm}`, + padding: `0 ${vars.spacing.component.sm}`, borderRadius: '6px', fontSize: vars.typography.fontSize['200'], }, [ButtonSize.xl]: { height: '64px', - padding: `0 ${vars.spacing.md}`, - borderRadius: vars.radius.lg, + padding: `0 ${vars.spacing.component.md}`, + borderRadius: vars.radius.component.lg, fontSize: vars.typography.fontSize['500'], }, },