From d9a58b0bad0456bc8f2499f0d331bfaed0632683 Mon Sep 17 00:00:00 2001 From: Sehwan Date: Tue, 14 Jan 2025 00:15:35 -0400 Subject: [PATCH 01/12] Started button component and storybook --- src/components/Button/Button.stories.ts | 129 ++++++++-- src/components/Button/Button.tsx | 308 ++++++++++++++++++++---- 2 files changed, 364 insertions(+), 73 deletions(-) diff --git a/src/components/Button/Button.stories.ts b/src/components/Button/Button.stories.ts index 8a27550..bb81d86 100644 --- a/src/components/Button/Button.stories.ts +++ b/src/components/Button/Button.stories.ts @@ -1,40 +1,125 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { expect, fn, userEvent } from "@storybook/test"; import { Button } from "./Button"; -const meta = { +const meta: Meta = { + title: "Components/Button", component: Button, - parameters: { - layout: "centered", + tags: ["autodocs"], + argTypes: { + variant: { + control: "select", + options: [ + "default", + "solid", + "outline", + "outlineGradient", + "accent", + "danger", + "warning", + ], + }, + size: { control: "select", options: ["small", "medium", "large"] }, + disabled: { control: "boolean" }, + loading: { control: "boolean" }, }, - args: { onClick: fn() }, -} satisfies Meta; +}; export default meta; -type Story = StoryObj; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: "Default Button", + }, +}; + +export const Solid: Story = { + args: { + variant: "solid", + children: "Solid Button", + }, +}; + +export const Outline: Story = { + args: { + variant: "outline", + children: "Outline Button", + }, +}; -export const Basic: Story = { +export const OutlineGradient: Story = { args: { - type: "button", - children: "Click", + variant: "outlineGradient", + children: "Outline Gradient Button", }, - play: async ({ args: { onClick }, canvas, step }) => { - const button = canvas.getByRole("button"); +}; - await step("renders a button with text", async () => { - expect(button).toHaveTextContent("Click"); - }); +export const Accent: Story = { + args: { + variant: "accent", + children: "Accent Button", + }, +}; - await step("calls onClick handler when clicked", async () => { - await userEvent.click(button); - expect(onClick).toHaveBeenCalledTimes(1); - }); +export const Danger: Story = { + args: { + variant: "danger", + children: "Danger Button", + }, +}; + +export const Warning: Story = { + args: { + variant: "warning", + children: "Warning Button", + }, +}; + +export const Small: Story = { + args: { + size: "small", + children: "Small Button", + }, +}; + +export const Medium: Story = { + args: { + size: "medium", + children: "Medium Button", + }, +}; + +export const Large: Story = { + args: { + size: "large", + children: "Large Button", + }, +}; + +// export const WithIcon: Story = { +// args: { +// children: "Button with Icon", +// icon: , +// }, +// }; + +export const Disabled: Story = { + args: { + children: "Disabled Button", + disabled: true, + }, +}; + +export const Loading: Story = { + args: { + children: "Loading Button", + loading: true, }, }; -export const Submit: Story = { +export const CustomStyled: Story = { args: { - type: "submit", - children: "Submit", + children: "Custom Styled Button", + customStyle: { backgroundColor: "red", color: "white", padding: "1rem" }, }, }; diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index a01eac2..849a3a7 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -1,55 +1,261 @@ -import { css } from "../../../styled-system/css"; +import React, { ReactNode, HTMLAttributes, forwardRef } from "react"; +import { css, cva } from "../../../styled-system/css"; +import ClipLoader from "react-spinners/ClipLoader"; +import { SystemStyleObject } from "@pandacss/types"; -export interface ButtonProps { - /** 버튼 텍스트 */ - children: React.ReactNode; - type?: "button" | "submit"; - onClick?: () => void; +type ButtonVariant = + | "solid" + | "outline" + | "outlineGradient" + | "default" + | "accent" + | "danger" + | "warning"; + +type ButtonSize = "small" | "medium" | "large"; + +export interface ButtonProps + extends Omit, "style"> { + children: ReactNode; + variant?: ButtonVariant; + size?: ButtonSize; + disabled?: boolean; + icon?: ReactNode; + loading?: boolean; + onClick?: (event: React.MouseEvent) => void; + onFocus?: (event: React.FocusEvent) => void; + onBlur?: (event: React.FocusEvent) => void; + customStyle?: SystemStyleObject; } -/** - * 버튼 컴포넌트입니다. - */ -export const Button = ({ - children, - type = "button", - onClick, - ...rest -}: ButtonProps) => { - return ( - + ); + } +); + +Button.displayName = "Button"; + +const styles = cva({ + variants: { + variant: { + default: { + backgroundColor: "bg.DEFAULT", + color: "text.DEFAULT", + "&:hover": { + backgroundColor: "bg.hover", + }, + "&:focus": { + outline: "2px solid", + outlineColor: "focus", + outlineOffset: "2px", + }, + "&:active": { + backgroundColor: "bg.active", + }, + "&:disabled": { + backgroundColor: "bg.disabled", + color: "text.disabled", + cursor: "not-allowed", + }, + }, + solid: { + backgroundColor: "solid.DEFAULT", + color: "text.contrast", + "&:hover": { + backgroundColor: "solid.hover", + }, + "&:focus": { + outline: "2px solid", + outlineColor: "focus", + outlineOffset: "2px", + }, + "&:active": { + backgroundColor: "solid.DEFAULT", + }, + "&:disabled": { + backgroundColor: "bg.disabled", + color: "text.disabled", + cursor: "not-allowed", + }, + }, + outline: { + backgroundColor: "transparent", + border: "2px solid", + borderColor: "border.DEFAULT", + color: "text.DEFAULT", + "&:hover": { + backgroundColor: "bg.hover", + }, + "&:focus": { + outline: "2px solid", + outlineColor: "focus", + outlineOffset: "2px", + }, + "&:active": { + backgroundColor: "bg.active", + }, + "&:disabled": { + borderColor: "border.disabled", + color: "text.disabled", + cursor: "not-allowed", + }, + }, + outlineGradient: { + backgroundColor: "transparent", + border: "2px solid", + borderColor: "teal.7", // Using the teal color for now because no specific gradient color defined in tokens for border yet. + color: "teal.7", + background: "linear-gradient(to right, teal.7, teal.9)", + backgroundClip: "text", + "-webkit-background-clip": "text", + "-webkit-text-fill-color": "transparent", + "&:hover": { + borderColor: "teal.9", + color: "teal.9", + background: "linear-gradient(to right, teal.9, teal.7)", + backgroundClip: "text", + "-webkit-background-clip": "text", + "-webkit-text-fill-color": "transparent", + }, + "&:focus": { + outline: "2px solid", + outlineColor: "focus", + outlineOffset: "2px", + }, + "&:active": { + background: "linear-gradient(to right, teal.7, teal.9)", + backgroundClip: "text", + "-webkit-background-clip": "text", + "-webkit-text-fill-color": "transparent", + }, + "&:disabled": { + borderColor: "border.disabled", + color: "text.disabled", + cursor: "not-allowed", + background: "none", + "-webkit-text-fill-color": "text.disabled", + }, + }, + accent: { + backgroundColor: "bg.accent", + color: "text.contrast", + "&:hover": { + backgroundColor: "bg.hover.accent", + }, + "&:focus": { + outline: "2px solid", + outlineColor: "focus", + outlineOffset: "2px", + }, + "&:active": { + backgroundColor: "bg.active.accent", + }, + "&:disabled": { + backgroundColor: "bg.disabled", + color: "text.disabled", + cursor: "not-allowed", + }, + }, + danger: { + backgroundColor: "bg.danger", + color: "text.contrast", + "&:hover": { + backgroundColor: "bg.hover.danger", + }, + "&:focus": { + outline: "2px solid", + outlineColor: "focus", + outlineOffset: "2px", + }, + "&:active": { + backgroundColor: "bg.active.danger", + }, + "&:disabled": { + backgroundColor: "bg.disabled", + color: "text.disabled", + cursor: "not-allowed", + }, + }, + warning: { + backgroundColor: "bg.warning", + color: "text.contrast", + "&:hover": { + backgroundColor: "bg.hover.warning", + }, + "&:focus": { + outline: "2px solid", + outlineColor: "focus", + outlineOffset: "2px", + }, + "&:active": { + backgroundColor: "bg.active.warning", + }, + "&:disabled": { + backgroundColor: "bg.disabled", + color: "text.disabled", + cursor: "not-allowed", + }, + }, + }, + size: { + small: { + fontSize: "sm", padding: "0.5rem 1rem", - background: "var(--button-bg-color)", - color: "var(--button-color)", - fontSize: "1rem", - fontWeight: 400, - textAlign: "center", - textDecoration: "none", - display: "inline-block", - width: ["auto", "100%"], - border: "none", - borderRadius: "4px", - boxShadow: - "0 4px 6px -1px rgba(0, 0, 0, 0.1),\n 0 2px 4px -1px rgba(0, 0, 0, 0.06)", - cursor: "pointer", - transition: "0.5s", - "&:active, &:hover, &:focus": { - background: "var(--button-hover-bg-color)", - outline: "0", - }, - "&:disabled": { opacity: 0.5 }, - })} - type={type} - onClick={onClick} - {...rest} - > - {children} - - ); -}; + }, + medium: { + fontSize: "md", + padding: "0.75rem 1.5rem", + }, + large: { + fontSize: "lg", + padding: "1rem 2rem", + }, + }, + }, +}); From 934a15328a9d8a10bdfe70652f0aaac42ad93a82 Mon Sep 17 00:00:00 2001 From: Sehwan Date: Sun, 19 Jan 2025 19:14:29 -0400 Subject: [PATCH 02/12] Made Button stories and update semantic colors for improved accessibility and consistency --- src/components/Button/Button.stories.ts | 133 ++-------- src/components/Button/Button.tsx | 335 ++++++++---------------- src/tokens/colors.ts | 44 +++- 3 files changed, 173 insertions(+), 339 deletions(-) diff --git a/src/components/Button/Button.stories.ts b/src/components/Button/Button.stories.ts index bb81d86..f74d1f1 100644 --- a/src/components/Button/Button.stories.ts +++ b/src/components/Button/Button.stories.ts @@ -1,125 +1,40 @@ import type { Meta, StoryObj } from "@storybook/react"; +import { expect, fn, userEvent } from "@storybook/test"; import { Button } from "./Button"; -const meta: Meta = { - title: "Components/Button", +const meta = { component: Button, - tags: ["autodocs"], - argTypes: { - variant: { - control: "select", - options: [ - "default", - "solid", - "outline", - "outlineGradient", - "accent", - "danger", - "warning", - ], - }, - size: { control: "select", options: ["small", "medium", "large"] }, - disabled: { control: "boolean" }, - loading: { control: "boolean" }, + parameters: { + layout: "centered", }, -}; + args: { onClick: fn() }, +} satisfies Meta; export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - children: "Default Button", - }, -}; - -export const Solid: Story = { - args: { - variant: "solid", - children: "Solid Button", - }, -}; - -export const Outline: Story = { - args: { - variant: "outline", - children: "Outline Button", - }, -}; - -export const OutlineGradient: Story = { - args: { - variant: "outlineGradient", - children: "Outline Gradient Button", - }, -}; - -export const Accent: Story = { - args: { - variant: "accent", - children: "Accent Button", - }, -}; - -export const Danger: Story = { - args: { - variant: "danger", - children: "Danger Button", - }, -}; - -export const Warning: Story = { - args: { - variant: "warning", - children: "Warning Button", - }, -}; +type Story = StoryObj; -export const Small: Story = { +export const Basic: Story = { args: { - size: "small", - children: "Small Button", + type: "button", + children: "시작하기", }, -}; + play: async ({ args: { onClick }, canvas, step }) => { + const button = canvas.getByRole("button"); -export const Medium: Story = { - args: { - size: "medium", - children: "Medium Button", - }, -}; + await step("renders a button with text", async () => { + expect(button).toHaveTextContent("시작하기"); + }); -export const Large: Story = { - args: { - size: "large", - children: "Large Button", + await step("calls onClick handler when clicked", async () => { + await userEvent.click(button); + expect(onClick).toHaveBeenCalledTimes(1); + }); }, }; -// export const WithIcon: Story = { -// args: { -// children: "Button with Icon", -// icon: , -// }, +// export const Submit: Story = { +// args: { +// type: "submit", +// children: "Submit", +// }, // }; - -export const Disabled: Story = { - args: { - children: "Disabled Button", - disabled: true, - }, -}; - -export const Loading: Story = { - args: { - children: "Loading Button", - loading: true, - }, -}; - -export const CustomStyled: Story = { - args: { - children: "Custom Styled Button", - customStyle: { backgroundColor: "red", color: "white", padding: "1rem" }, - }, -}; diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 849a3a7..f5e2fd0 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -1,260 +1,145 @@ -import React, { ReactNode, HTMLAttributes, forwardRef } from "react"; +import React from "react"; import { css, cva } from "../../../styled-system/css"; -import ClipLoader from "react-spinners/ClipLoader"; -import { SystemStyleObject } from "@pandacss/types"; +import type { SystemStyleObject } from "@pandacss/types"; +// import { colors } from "../../tokens/colors"; type ButtonVariant = | "solid" | "outline" - | "outlineGradient" + | "outline-gradient" | "default" | "accent" | "danger" | "warning"; -type ButtonSize = "small" | "medium" | "large"; - -export interface ButtonProps - extends Omit, "style"> { - children: ReactNode; +export interface ButtonProps { + /** 버튼 텍스트 */ + children: React.ReactNode; + type?: "button" | "submit"; + onClick?: () => void; variant?: ButtonVariant; - size?: ButtonSize; - disabled?: boolean; - icon?: ReactNode; - loading?: boolean; - onClick?: (event: React.MouseEvent) => void; - onFocus?: (event: React.FocusEvent) => void; - onBlur?: (event: React.FocusEvent) => void; - customStyle?: SystemStyleObject; + style?: SystemStyleObject; // Add style prop for custom inline styles } -export const Button = forwardRef( - ( - { - children, - variant = "default", - size = "medium", - disabled = false, - icon, - loading = false, - onClick, - onFocus, - onBlur, - ...rest - }, - ref - ) => { - return ( - - ); - } -); +/** + * 버튼 컴포넌트입니다. + */ +export const Button = ({ + children, + type = "button", + onClick, + variant = "default", + style, // destructure the style prop + ...rest +}: ButtonProps) => { + return ( + + ); +}; -Button.displayName = "Button"; +const baseStyles = { + appearance: "none", + margin: "0", + padding: "0.7rem 3rem", + fontSize: "1.5rem", + fontWeight: 500, + textAlign: "center", + textDecoration: "none", + display: "flex", + alignItems: "center", + justifyContent: "center", + width: ["auto", "100%"], + borderRadius: "10px", + cursor: "pointer", + transition: "0.2s", + lineHeight: "1", + outline: "0", + "&:focus": { + outlineColor: "focus", + outline: "3px solid", + outlineOffset: "2px", + borderRadius: "10px", + }, + "&:disabled": { opacity: 0.5 }, +}; const styles = cva({ variants: { variant: { default: { - backgroundColor: "bg.DEFAULT", - color: "text.DEFAULT", - "&:hover": { - backgroundColor: "bg.hover", - }, - "&:focus": { - outline: "2px solid", - outlineColor: "focus", - outlineOffset: "2px", - }, - "&:active": { - backgroundColor: "bg.active", - }, - "&:disabled": { - backgroundColor: "bg.disabled", - color: "text.disabled", - cursor: "not-allowed", - }, - }, - solid: { - backgroundColor: "solid.DEFAULT", - color: "text.contrast", - "&:hover": { - backgroundColor: "solid.hover", - }, - "&:focus": { - outline: "2px solid", - outlineColor: "focus", - outlineOffset: "2px", - }, - "&:active": { - backgroundColor: "solid.DEFAULT", - }, - "&:disabled": { - backgroundColor: "bg.disabled", - color: "text.disabled", - cursor: "not-allowed", - }, - }, - outline: { - backgroundColor: "transparent", - border: "2px solid", - borderColor: "border.DEFAULT", - color: "text.DEFAULT", - "&:hover": { - backgroundColor: "bg.hover", - }, - "&:focus": { - outline: "2px solid", - outlineColor: "focus", - outlineOffset: "2px", - }, - "&:active": { - backgroundColor: "bg.active", - }, - "&:disabled": { - borderColor: "border.disabled", - color: "text.disabled", - cursor: "not-allowed", - }, - }, - outlineGradient: { - backgroundColor: "transparent", - border: "2px solid", - borderColor: "teal.7", // Using the teal color for now because no specific gradient color defined in tokens for border yet. - color: "teal.7", - background: "linear-gradient(to right, teal.7, teal.9)", - backgroundClip: "text", - "-webkit-background-clip": "text", - "-webkit-text-fill-color": "transparent", - "&:hover": { - borderColor: "teal.9", - color: "teal.9", - background: "linear-gradient(to right, teal.9, teal.7)", - backgroundClip: "text", - "-webkit-background-clip": "text", - "-webkit-text-fill-color": "transparent", - }, - "&:focus": { - outline: "2px solid", - outlineColor: "focus", - outlineOffset: "2px", - }, - "&:active": { - background: "linear-gradient(to right, teal.7, teal.9)", - backgroundClip: "text", - "-webkit-background-clip": "text", - "-webkit-text-fill-color": "transparent", - }, - "&:disabled": { - borderColor: "border.disabled", - color: "text.disabled", - cursor: "not-allowed", - background: "none", - "-webkit-text-fill-color": "text.disabled", + background: "bg", + color: "text", + "&:active, &:hover": { + background: "bg.hover", + outline: "0", }, }, accent: { - backgroundColor: "bg.accent", - color: "text.contrast", - "&:hover": { - backgroundColor: "bg.hover.accent", - }, - "&:focus": { - outline: "2px solid", - outlineColor: "focus", - outlineOffset: "2px", - }, - "&:active": { - backgroundColor: "bg.active.accent", - }, - "&:disabled": { - backgroundColor: "bg.disabled", - color: "text.disabled", - cursor: "not-allowed", + background: "bg.accent", + color: "text.accent", + "&:active, &:hover": { + background: "bg.hover.accent", + outline: "0", }, }, danger: { - backgroundColor: "bg.danger", - color: "text.contrast", - "&:hover": { - backgroundColor: "bg.hover.danger", - }, - "&:focus": { - outline: "2px solid", - outlineColor: "focus", - outlineOffset: "2px", - }, - "&:active": { - backgroundColor: "bg.active.danger", - }, - "&:disabled": { - backgroundColor: "bg.disabled", - color: "text.disabled", - cursor: "not-allowed", + background: "bg.danger", + color: "text.danger", + "&:active, &:hover": { + background: "bg.hover.danger", + outline: "0", }, }, warning: { - backgroundColor: "bg.warning", - color: "text.contrast", - "&:hover": { - backgroundColor: "bg.hover.warning", - }, - "&:focus": { - outline: "2px solid", - outlineColor: "focus", - outlineOffset: "2px", - }, - "&:active": { - backgroundColor: "bg.active.warning", - }, - "&:disabled": { - backgroundColor: "bg.disabled", - color: "text.disabled", - cursor: "not-allowed", + background: "bg.warning", + color: "text.warning", + "&:active, &:hover": { + background: "bg.hover.warning", + outline: "0", }, }, - }, - size: { - small: { - fontSize: "sm", - padding: "0.5rem 1rem", + solid: { + background: "bg.solid", + color: "text.solid", + "&:active, &:hover": { + background: "bg.hover.solid", + outline: "0", + }, }, - medium: { - fontSize: "md", - padding: "0.75rem 1.5rem", + outline: { + background: "bg.outline", + color: "text.outline", + border: "4px solid", + borderColor: "border.outline", + "&:active, &:hover": { + background: "bg.hover.outline", + outline: "0", + }, }, - large: { - fontSize: "lg", - padding: "1rem 2rem", + "outline-gradient": { + "--gradient-color": "linear-gradient(135deg, #24eaca, #846de9)", + background: "transparent", + color: "text.outline", + border: "4px solid transparent", + backgroundClip: "padding-box, border-box", + backgroundOrigin: "padding-box, border-box", + borderImage: "var(--gradient-color)", + borderImageSlice: "1", + borderImageOutset: "0", + "&:active, &:hover": { + outline: "0", + }, }, }, }, diff --git a/src/tokens/colors.ts b/src/tokens/colors.ts index 2aff6eb..1b13553 100644 --- a/src/tokens/colors.ts +++ b/src/tokens/colors.ts @@ -9,10 +9,10 @@ export const semanticColors: SemanticTokens["colors"] = { bg: { DEFAULT: { DEFAULT: { - value: { base: "{colors.gray.3}", _dark: "{colors.grayDark.3}" }, + value: { base: "{colors.grayDark.3}", _dark: "{colors.gray.3}" }, }, accent: { - value: { base: "{colors.teal.3}", _dark: "{colors.violetDark.3}" }, + value: { base: "{colors.teal.5}", _dark: "{colors.tealDark.8}" }, }, danger: { value: { base: "{colors.red.3}", _dark: "{colors.redDark.3}" }, @@ -20,10 +20,19 @@ export const semanticColors: SemanticTokens["colors"] = { warning: { value: { base: "{colors.yellow.3}", _dark: "{colors.yellowDark.3}" }, }, + solid: { + value: { base: "{colors.violet.10}", _dark: "{colors.violet.1}" }, + }, + outline: { + value: { base: "{colors.violet.2}", _dark: "{colors.violetDark.8}" }, + }, + "outline-gradient": { + value: { base: "{colors.violet.2}", _dark: "{colors.violetDark.8}" }, + }, }, hover: { DEFAULT: { - value: { base: "{colors.gray.4}", _dark: "{colors.grayDark.4}" }, + value: { base: "{colors.grayDark.8}", _dark: "{colors.gray.6}" }, }, accent: { value: { base: "{colors.teal.4}", _dark: "{colors.violetDark.4}" }, @@ -34,6 +43,12 @@ export const semanticColors: SemanticTokens["colors"] = { warning: { value: { base: "{colors.yellow.4}", _dark: "{colors.yellowDark.4}" }, }, + solid: { + value: { base: "{colors.violet.8}", _dark: "{colors.violet.3}" }, + }, + outline: { + value: { base: "{colors.violet.4}", _dark: "{colors.violetDark.10}" }, + }, }, active: { DEFAULT: { @@ -64,6 +79,9 @@ export const semanticColors: SemanticTokens["colors"] = { warning: { value: { base: "{colors.yellow.7}", _dark: "{colors.yellowDark.7}" }, }, + outline: { + value: { base: "{colors.violetDark.10}", _dark: "{colors.violet.7}" }, + }, }, hover: { DEFAULT: { @@ -116,7 +134,7 @@ export const semanticColors: SemanticTokens["colors"] = { value: { base: "{colors.gray.11}", _dark: "{colors.grayDark.11}" }, }, accent: { - value: { base: "{colors.teal.11}", _dark: "{colors.violetDark.11}" }, + value: { base: "{colors.teal.11}", _dark: "{colors.tealDark.11}" }, }, danger: { value: { base: "{colors.red.11}", _dark: "{colors.redDark.11}" }, @@ -127,7 +145,7 @@ export const semanticColors: SemanticTokens["colors"] = { }, DEFAULT: { DEFAULT: { - value: { base: "{colors.gray.12}", _dark: "{colors.grayDark.12}" }, + value: { base: "{colors.grayDark.12}", _dark: "{colors.gray.12}" }, }, accent: { value: { base: "{colors.teal.12}", _dark: "{colors.violetDark.12}" }, @@ -138,6 +156,22 @@ export const semanticColors: SemanticTokens["colors"] = { warning: { value: { base: "{colors.yellow.12}", _dark: "{colors.yellowDark.12}" }, }, + solid: { + value: { base: "{colors.violet.1}", _dark: "{colors.violet.10}" }, + }, + outline: { + value: { + base: "{colors.violetDark.1}", + _dark: "{colors.violet.1}", + }, + }, + }, + }, + focus: { + DEFAULT: { + DEFAULT: { + value: { base: "{colors.violet.10}", _dark: "{colors.violet.10}" }, + }, }, }, }; From 5aca818502e413980c4af4317767c2e402ae62f9 Mon Sep 17 00:00:00 2001 From: Sehwan Date: Mon, 20 Jan 2025 12:51:08 -0400 Subject: [PATCH 03/12] Refactor Button component styles for improved color semantics and accessibility --- src/components/Button/Button.tsx | 51 +++++++++++++++++++------------- src/tokens/colors.ts | 34 --------------------- 2 files changed, 31 insertions(+), 54 deletions(-) diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index f5e2fd0..6693cae 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -66,10 +66,9 @@ const baseStyles = { lineHeight: "1", outline: "0", "&:focus": { - outlineColor: "focus", + outlineColor: { base: "{colors.violet.10}", _dark: "{colors.violet.7}" }, outline: "3px solid", outlineOffset: "2px", - borderRadius: "10px", }, "&:disabled": { opacity: 0.5 }, }; @@ -82,7 +81,6 @@ const styles = cva({ color: "text", "&:active, &:hover": { background: "bg.hover", - outline: "0", }, }, accent: { @@ -90,7 +88,6 @@ const styles = cva({ color: "text.accent", "&:active, &:hover": { background: "bg.hover.accent", - outline: "0", }, }, danger: { @@ -98,7 +95,6 @@ const styles = cva({ color: "text.danger", "&:active, &:hover": { background: "bg.hover.danger", - outline: "0", }, }, warning: { @@ -106,40 +102,55 @@ const styles = cva({ color: "text.warning", "&:active, &:hover": { background: "bg.hover.warning", - outline: "0", }, }, solid: { - background: "bg.solid", - color: "text.solid", + background: { base: "{colors.violet.9}", _dark: "{colors.violet.9}" }, + color: { base: "{colors.violet.1}", _dark: "{colors.violet.1}" }, "&:active, &:hover": { - background: "bg.hover.solid", - outline: "0", + background: { + base: "{colors.violet.8}", + _dark: "{colors.violetDark.10}", + }, }, }, outline: { - background: "bg.outline", - color: "text.outline", + background: { + base: "{colors.violet.2}", + _dark: "{colors.violetDark.8}", + }, + color: { + base: "{colors.violetDark.1}", + _dark: "{colors.violet.1}", + }, border: "4px solid", - borderColor: "border.outline", + borderColor: { + base: "{colors.violetDark.10}", + _dark: "{colors.violet.7}", + }, "&:active, &:hover": { - background: "bg.hover.outline", - outline: "0", + background: { + base: "{colors.violet.4}", + _dark: "{colors.violetDark.10}", + }, }, }, "outline-gradient": { - "--gradient-color": "linear-gradient(135deg, #24eaca, #846de9)", + "--gradient-color": + "linear-gradient(90deg,{colors.teal.9},{colors.violet.10})", background: "transparent", - color: "text.outline", + color: { + base: "{colors.violetDark.1}", + _dark: "{colors.violet.1}", + }, border: "4px solid transparent", + borderRadius: "10px", backgroundClip: "padding-box, border-box", backgroundOrigin: "padding-box, border-box", borderImage: "var(--gradient-color)", borderImageSlice: "1", borderImageOutset: "0", - "&:active, &:hover": { - outline: "0", - }, + "&:active, &:hover": {}, }, }, }, diff --git a/src/tokens/colors.ts b/src/tokens/colors.ts index 1b13553..02df3ab 100644 --- a/src/tokens/colors.ts +++ b/src/tokens/colors.ts @@ -20,15 +20,6 @@ export const semanticColors: SemanticTokens["colors"] = { warning: { value: { base: "{colors.yellow.3}", _dark: "{colors.yellowDark.3}" }, }, - solid: { - value: { base: "{colors.violet.10}", _dark: "{colors.violet.1}" }, - }, - outline: { - value: { base: "{colors.violet.2}", _dark: "{colors.violetDark.8}" }, - }, - "outline-gradient": { - value: { base: "{colors.violet.2}", _dark: "{colors.violetDark.8}" }, - }, }, hover: { DEFAULT: { @@ -43,12 +34,6 @@ export const semanticColors: SemanticTokens["colors"] = { warning: { value: { base: "{colors.yellow.4}", _dark: "{colors.yellowDark.4}" }, }, - solid: { - value: { base: "{colors.violet.8}", _dark: "{colors.violet.3}" }, - }, - outline: { - value: { base: "{colors.violet.4}", _dark: "{colors.violetDark.10}" }, - }, }, active: { DEFAULT: { @@ -79,9 +64,6 @@ export const semanticColors: SemanticTokens["colors"] = { warning: { value: { base: "{colors.yellow.7}", _dark: "{colors.yellowDark.7}" }, }, - outline: { - value: { base: "{colors.violetDark.10}", _dark: "{colors.violet.7}" }, - }, }, hover: { DEFAULT: { @@ -156,22 +138,6 @@ export const semanticColors: SemanticTokens["colors"] = { warning: { value: { base: "{colors.yellow.12}", _dark: "{colors.yellowDark.12}" }, }, - solid: { - value: { base: "{colors.violet.1}", _dark: "{colors.violet.10}" }, - }, - outline: { - value: { - base: "{colors.violetDark.1}", - _dark: "{colors.violet.1}", - }, - }, - }, - }, - focus: { - DEFAULT: { - DEFAULT: { - value: { base: "{colors.violet.10}", _dark: "{colors.violet.10}" }, - }, }, }, }; From 2a5b3781c49ecb3fdbbfb33f8649415c87283b09 Mon Sep 17 00:00:00 2001 From: Sehwan Date: Wed, 22 Jan 2025 12:59:47 -0400 Subject: [PATCH 04/12] feat: enhance Button component with tone and variant options --- src/components/Button/Button.stories.ts | 32 +----- src/components/Button/Button.tsx | 129 +++++++++++------------- src/tokens/colors.ts | 2 + 3 files changed, 64 insertions(+), 99 deletions(-) diff --git a/src/components/Button/Button.stories.ts b/src/components/Button/Button.stories.ts index f74d1f1..13d4483 100644 --- a/src/components/Button/Button.stories.ts +++ b/src/components/Button/Button.stories.ts @@ -1,40 +1,14 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { expect, fn, userEvent } from "@storybook/test"; import { Button } from "./Button"; -const meta = { +export default { component: Button, parameters: { layout: "centered", }, - args: { onClick: fn() }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Basic: Story = { args: { - type: "button", children: "시작하기", }, - play: async ({ args: { onClick }, canvas, step }) => { - const button = canvas.getByRole("button"); - - await step("renders a button with text", async () => { - expect(button).toHaveTextContent("시작하기"); - }); - - await step("calls onClick handler when clicked", async () => { - await userEvent.click(button); - expect(onClick).toHaveBeenCalledTimes(1); - }); - }, -}; +} satisfies Meta; -// export const Submit: Story = { -// args: { -// type: "submit", -// children: "Submit", -// }, -// }; +export const Basic: StoryObj = {}; diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 6693cae..f10d425 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -1,23 +1,23 @@ -import React from "react"; +import React, { type HTMLAttributes } from "react"; import { css, cva } from "../../../styled-system/css"; import type { SystemStyleObject } from "@pandacss/types"; -// import { colors } from "../../tokens/colors"; -type ButtonVariant = - | "solid" - | "outline" - | "outline-gradient" - | "default" - | "accent" - | "danger" - | "warning"; +type ButtonVariant = "solid" | "outline" | "transparent"; -export interface ButtonProps { - /** 버튼 텍스트 */ +type ButtonTone = "primary" | "neutral" | "accent" | "danger" | "warning"; + +export interface ButtonProps + extends Omit, "style"> { + /** 텍스트 */ children: React.ReactNode; + /** 타입 */ type?: "button" | "submit"; onClick?: () => void; - variant?: ButtonVariant; + /** 종류 */ + variant: ButtonVariant; + /** 색조 */ + tone?: ButtonTone; + /** 추가 스타일 */ style?: SystemStyleObject; // Add style prop for custom inline styles } @@ -28,14 +28,15 @@ export const Button = ({ children, type = "button", onClick, - variant = "default", + variant = "solid", + tone = "primary", style, // destructure the style prop ...rest }: ButtonProps) => { return ( + + + ); + }, + argTypes: { + children: { + control: false, + }, + variant: { + control: "radio", + options: ["solid", "outline"], + }, + }, +}; + +export const Tones: StoryObj = { + render: (args) => { + return ( +
+ + + + +
+ ); + }, + argTypes: { + children: { + control: false, + }, + tone: { + control: "radio", + options: ["neutral", "accent", "danger", "warning"], + }, + }, +}; + +export const Sizes: StoryObj = { + render: (args) => { + return ( +
+ + + +
+ ); + }, + argTypes: { + children: { + control: false, + }, + size: { + control: "radio", + options: ["small", "medium", "large"], + }, + }, +}; + +export const Disabled: StoryObj = { + render: (args) => { + return ( +
+ + +
+ ); + }, + argTypes: { + children: { + control: false, + }, + disabled: { + control: "boolean", + }, + }, +}; diff --git a/src/components/Button/Button.test.tsx b/src/components/Button/Button.test.tsx index c375530..acaa49d 100644 --- a/src/components/Button/Button.test.tsx +++ b/src/components/Button/Button.test.tsx @@ -1,23 +1,108 @@ -import { render, screen } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { expect, describe, it, vi } from "vitest"; +import { composeStories } from "@storybook/react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { expect, test, vi } from "vitest"; +import * as stories from "./Button.stories"; import { Button } from "./Button"; -describe("); +const { Basic, Variants, Tones, Sizes, Disabled } = composeStories(stories); - expect(screen.getByRole("button")).toHaveTextContent("Click"); - }); +test("renders the button with the correct text content", () => { + render(테스트); - it("calls onClick handler when clicked", async () => { - const user = userEvent.setup(); - const handleClick = vi.fn(); + expect(screen.getByText("테스트")).toBeInTheDocument(); +}); + +test("applies the correct variant styles", () => { + render(); + + expect(screen.getByText("솔리드 버튼")).toHaveClass("bg_bg"); + expect(screen.getByText("아웃라인 버튼")).toHaveClass("bd_3px_solid"); +}); + +test("applies the correct tone styles", () => { + render(); + + expect(screen.getByText("중립 색조")).toHaveClass("bg_bg"); + expect(screen.getByText("강조 색조")).toHaveClass("bg_bg.accent"); + expect(screen.getByText("위험 색조")).toHaveClass("bg_bg.danger"); + expect(screen.getByText("경고 색조")).toHaveClass("bg_bg.warning"); +}); + +test("applies the correct font size based on the size prop", () => { + render(); + + expect(screen.getByText("작은 버튼")).toHaveClass("fs_sm"); + expect(screen.getByText("중간 버튼")).toHaveClass("fs_md"); + expect(screen.getByText("큰 버튼")).toHaveClass("fs_lg"); +}); + +test("applies the correct disabled styles", () => { + render(); - render(); + expect(screen.getByText("비활성화 버튼")).toBeDisabled(); + expect(screen.getByText("활성화 버튼")).toBeEnabled(); + expect(screen.getByText("비활성화 버튼")).toHaveClass("[&:disabled]:op_0.5"); +}); + +test("renders a button with type='button' by default", () => { + render(Default Button); + const button = screen.getByText("Default Button"); + expect(button).toHaveAttribute("type", "button"); +}); - await user.click(screen.getByRole("button")); +test("renders a button with type='button' by default", () => { + render(Default Button); + const button = screen.getByText("Default Button"); + expect(button).toHaveAttribute("type", "button"); +}); + +test("renders a button with type='button' when specified", () => { + render( + + ); + const button = screen.getByText("Button Type Button"); + expect(button).toHaveAttribute("type", "button"); +}); + +test("renders a button with type='submit' when specified", () => { + render( +
+ +
+ ); + const button = screen.getByText("Submit Type Button"); + expect(button).toHaveAttribute("type", "submit"); +}); + +test("submits the form when type='submit' button is clicked", () => { + const handleSubmit = vi.fn(); + render( +
+ +
+ ); + + const submitButton = screen.getByText("Submit Button"); + fireEvent.click(submitButton); + expect(handleSubmit).toHaveBeenCalledTimes(1); +}); - expect(handleClick).toHaveBeenCalledTimes(1); - }); +test("does not submit the form when type='button' button is clicked", () => { + const handleSubmit = vi.fn(); + render( +
+ +
+ ); + const buttonTypeButton = screen.getByText("Button Type Button"); + fireEvent.click(buttonTypeButton); + expect(handleSubmit).toHaveBeenCalledTimes(0); }); diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 29709df..3eafe25 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -3,8 +3,8 @@ import { css, cva } from "../../../styled-system/css"; import type { SystemStyleObject } from "@pandacss/types"; type ButtonVariant = "solid" | "outline"; - type ButtonTone = "neutral" | "accent" | "danger" | "warning"; +type ButtonSize = "small" | "medium" | "large"; export interface ButtonProps extends Omit, "style"> { @@ -12,17 +12,26 @@ export interface ButtonProps children: React.ReactNode; /** 타입 */ type?: "button" | "submit"; + /** 클릭 시 실행함수 */ onClick?: () => void; /** 종류 */ variant: ButtonVariant; /** 색조 */ tone?: ButtonTone; + /** 버튼의 크기 */ + size?: ButtonSize; /** 추가 스타일 */ - style?: SystemStyleObject; // Add style prop for custom inline styles + style?: SystemStyleObject; + /** 버튼 비활성화 여부 */ + disabled?: boolean; } /** - * 버튼 컴포넌트입니다. + * - `variant` 속성으로 버튼의 스타일 종류를 지정할 수 있습니다. (solid, outline) + * - `tone` 속성으로 버튼의 색상 강조를 지정할 수 있습니다. (neutral, accent, danger, warning) + * - `size` 속성으로 버튼의 크기를 지정할 수 있습니다. (small, medium, large) + * - `type` 속성으로 버튼의 타입을 지정할 수 있습니다. (button, submit) + * - `disabled` 속성을 사용하여 버튼을 비활성화할 수 있습니다. */ export const Button = ({ children, @@ -30,18 +39,21 @@ export const Button = ({ onClick, variant = "solid", tone = "neutral", - style, // destructure the style prop + style, + size = "medium", + disabled, ...rest }: ButtonProps) => { return ( +