diff --git a/.storybook/preview.module.scss b/.storybook/preview.module.scss index a776076..6b67abb 100644 --- a/.storybook/preview.module.scss +++ b/.storybook/preview.module.scss @@ -1,3 +1,7 @@ +body { + max-width: initial; +} + .Wrapper { display: flex; flex-flow: row wrap; @@ -10,5 +14,13 @@ .Story { height: 100%; padding: 2.5rem; - background-color: gray; + background-color: white; + color: black; +} + +.InverseStory { + height: 100%; + padding: 2.5rem; + background-color: black; + color: white; } diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 1c9de14..4ad260b 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,5 +1,7 @@ import type { Preview } from "@storybook/react"; +import "@/styles/reset.scss"; +import "@/styles/global.scss"; import styles from "./preview.module.scss"; const preview: Preview = { @@ -18,6 +20,10 @@ const preview: Preview = {
+ +
+ +
), ], diff --git a/src/App.module.scss b/src/App.module.scss new file mode 100644 index 0000000..674c893 --- /dev/null +++ b/src/App.module.scss @@ -0,0 +1,23 @@ +.Test { + display: flex; + flex-direction: column; + gap: 16px; + width: 300px; + align-items: center; + margin: 0 auto; + margin-top: 40px; +} + +.Test2 { + width: 161px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.Test3 { + width: 99px; + display: flex; + flex-direction: column; + gap: 16px; +} diff --git a/src/App.tsx b/src/App.tsx index 729b1ea..1a8ea41 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,23 @@ +import styles from "./App.module.scss"; + +import Button from "@/components/ui/Button/Button"; +import IconButton from "@/components/ui/IconButton/IconButton"; + const App = () => { - return
App
; + return ( +
+
+ ); }; export default App; diff --git a/src/components/ui/BaseButton/BaseButton.module.scss b/src/components/ui/BaseButton/BaseButton.module.scss new file mode 100644 index 0000000..113a1d4 --- /dev/null +++ b/src/components/ui/BaseButton/BaseButton.module.scss @@ -0,0 +1,9 @@ +@use "@/styles/mixins" as *; + +.BaseButton { + cursor: pointer; + border-radius: 0.875rem; + width: 100%; + height: 3.25rem; + padding: 0.875rem; +} diff --git a/src/components/ui/BaseButton/BaseButton.tsx b/src/components/ui/BaseButton/BaseButton.tsx new file mode 100644 index 0000000..51ec0e5 --- /dev/null +++ b/src/components/ui/BaseButton/BaseButton.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +import classNames from "classnames"; + +import styles from "@/components/ui/BaseButton/BaseButton.module.scss"; +import type { BaseButtonProps } from "@/components/ui/BaseButton/BaseButton.types"; + +const BaseButton = React.forwardRef( + ({ className, children, type = "button", ...props }, ref) => { + return ( + + ); + }, +); + +export default BaseButton; diff --git a/src/components/ui/BaseButton/BaseButton.types.ts b/src/components/ui/BaseButton/BaseButton.types.ts new file mode 100644 index 0000000..6ecb4da --- /dev/null +++ b/src/components/ui/BaseButton/BaseButton.types.ts @@ -0,0 +1,6 @@ +export interface ButtonOwnProps { + as?: React.ElementType; + text?: string; +} + +export type BaseButtonProps = React.ButtonHTMLAttributes; diff --git a/src/components/ui/Button/Button.module.scss b/src/components/ui/Button/Button.module.scss new file mode 100644 index 0000000..723d0b7 --- /dev/null +++ b/src/components/ui/Button/Button.module.scss @@ -0,0 +1,45 @@ +@use "@/styles/mixins" as *; + +.Button { + @include buttonSecondary; + + &.style-primary { + background-color: var(--color-text01); + color: var(--color-white); + } + + &.style-secondary { + background-color: var(--color-gray400); + color: var(--color-white); + } + + &.style-tertiary { + background-color: var(--color-gray200); + color: var(--color-text02); + } + + &:disabled { + background-color: var(--color-gray350); + color: var(--color-text03); + cursor: not-allowed; + } +} + +.ButtonStory { + display: flex; + flex-direction: column; + gap: 1rem; + + .Wrapper { + display: flex; + gap: 1rem; + justify-content: center; + align-items: center; + width: 300px; + } + + .Text { + font-size: 1.125rem; + width: 120px; + } +} diff --git a/src/components/ui/Button/Button.stories.tsx b/src/components/ui/Button/Button.stories.tsx new file mode 100644 index 0000000..4811210 --- /dev/null +++ b/src/components/ui/Button/Button.stories.tsx @@ -0,0 +1,43 @@ +import Button from "@/components/ui/Button/Button"; +import styles from "@/components/ui/Button/Button.module.scss"; +import type { ButtonProps } from "@/components/ui/Button/Button.types"; + +import type { Meta, StoryObj } from "@storybook/react"; + +const meta: Meta = { + title: "Example/Button", + component: Button, + parameters: { + layout: "centered", + }, + tags: ["!autodocs"], +}; + +export default meta; + +export const Primary: StoryObj = { + args: { + text: "정보가 맞아요", + disabled: false, + variant: "primary", + }, +}; + +export const VariantProps: StoryObj = { + render: () => ( +
+
+

primary

+
+
+

secondary

+
+
+

tertiary

+
+
+ ), +}; diff --git a/src/components/ui/Button/Button.tsx b/src/components/ui/Button/Button.tsx new file mode 100644 index 0000000..6b49acf --- /dev/null +++ b/src/components/ui/Button/Button.tsx @@ -0,0 +1,39 @@ +import { forwardRef, useCallback } from "react"; + +import classNames from "classnames"; + +import BaseButton from "@/components/ui/BaseButton/BaseButton"; +import styles from "@/components/ui/Button/Button.module.scss"; +import type { ButtonProps } from "@/components/ui/Button/Button.types"; + +const Button = forwardRef( + ( + { as = BaseButton, className, variant = "primary", text, disabled = false, onClick, ...props }, + ref, + ) => { + const Comp = as as typeof BaseButton; + + const handleClick = useCallback( + (e: React.MouseEvent) => { + if (!disabled) { + onClick?.(e); + } + }, + [onClick, disabled], + ); + + return ( + + {text} + + ); + }, +); + +export default Button; diff --git a/src/components/ui/Button/Button.types.ts b/src/components/ui/Button/Button.types.ts new file mode 100644 index 0000000..5adc592 --- /dev/null +++ b/src/components/ui/Button/Button.types.ts @@ -0,0 +1,7 @@ +import type { ButtonOwnProps } from "@/components/ui/BaseButton/BaseButton.types"; + +type ButtonVariant = "primary" | "secondary" | "tertiary"; + +export interface ButtonProps extends React.ButtonHTMLAttributes, ButtonOwnProps { + variant?: ButtonVariant; +} diff --git a/src/components/ui/Icon/Icon.module.scss b/src/components/ui/Icon/Icon.module.scss new file mode 100644 index 0000000..611042c --- /dev/null +++ b/src/components/ui/Icon/Icon.module.scss @@ -0,0 +1,20 @@ +.IconStory { + display: flex; + align-items: center; + gap: 4rem; + + .Wrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + + .InnerWrapper { + width: 1.5rem; + height: 1.5rem; + display: flex; + justify-content: center; + align-items: center; + } + } +} diff --git a/src/components/ui/Icon/Icon.stories.tsx b/src/components/ui/Icon/Icon.stories.tsx index 1e1ccbe..12e19ff 100644 --- a/src/components/ui/Icon/Icon.stories.tsx +++ b/src/components/ui/Icon/Icon.stories.tsx @@ -1,6 +1,7 @@ import Icon from "@/components/ui/Icon/Icon"; import { ICONS } from "@/components/ui/Icon/Icon"; -import type { IconNameType } from "@/components/ui/Icon/Icon"; +import type { IconNameType, IconProps } from "@/components/ui/Icon/Icon"; +import styles from "@/components/ui/Icon/Icon.module.scss"; import type { Meta, StoryObj } from "@storybook/react"; @@ -10,34 +11,23 @@ const meta: Meta = { parameters: { layout: "centered", }, - argTypes: { - name: { - control: { - type: "select", - options: ["camera", "close", "gallery", "leftArrow", "paste", "plus"], - }, - }, - }, + tags: ["!autodocs"], }; export default meta; -export const AllIcons: StoryObj = { +export const Primary: StoryObj = { + args: { + name: "camera", + }, +}; + +export const AllIcons: StoryObj = { render: () => ( -
+
{(Object.keys(ICONS) as IconNameType[]).map((iconName) => ( -
-
+
+

{iconName}

diff --git a/src/components/ui/Icon/Icon.tsx b/src/components/ui/Icon/Icon.tsx index c23ff85..e22f3a4 100644 --- a/src/components/ui/Icon/Icon.tsx +++ b/src/components/ui/Icon/Icon.tsx @@ -7,6 +7,10 @@ import PlusIcon from "@/assets/svg/ic-plus.svg?react"; export type IconNameType = "camera" | "close" | "gallery" | "leftArrow" | "paste" | "plus"; +export interface IconProps { + name: IconNameType; +} + export const ICONS = { camera: CameraIcon, close: CloseIcon, @@ -17,7 +21,7 @@ export const ICONS = { }; // 추후 사이즈, 컬러등 추가 가능 -const Icon = ({ name }: { name: IconNameType }) => { +const Icon = ({ name }: IconProps) => { const SvgIcon = ICONS[name]; return ; diff --git a/src/components/ui/IconButton/IconButton.module.scss b/src/components/ui/IconButton/IconButton.module.scss new file mode 100644 index 0000000..f1f51d8 --- /dev/null +++ b/src/components/ui/IconButton/IconButton.module.scss @@ -0,0 +1,42 @@ +@use "@/styles/mixins" as *; + +.IconButton { + display: flex; + justify-content: center; + align-items: center; + + &.size-md { + gap: 0.375rem; + background-color: var(--color-gray400); + color: var(--color-white); + @include buttonSecondary; + } + + &.size-sm { + gap: 0.125rem; + height: 2.375rem; + padding: 0.5rem 0.875rem; + background-color: var(--color-text01); + color: var(--color-white); + border-radius: 0.75rem; + @include buttonTertiary; + } +} + +.IconButtonStory { + display: flex; + flex-direction: column; + gap: 1rem; + + .Wrapper { + display: flex; + gap: 1rem; + justify-content: center; + align-items: center; + } + + .Text { + font-size: 1.125rem; + width: 120px; + } +} diff --git a/src/components/ui/IconButton/IconButton.stories.tsx b/src/components/ui/IconButton/IconButton.stories.tsx new file mode 100644 index 0000000..f897bed --- /dev/null +++ b/src/components/ui/IconButton/IconButton.stories.tsx @@ -0,0 +1,50 @@ +import IconButton from "@/components/ui/IconButton/IconButton"; +import styles from "@/components/ui/IconButton/IconButton.module.scss"; +import type { IconButtonProps } from "@/components/ui/IconButton/IconButton.types"; + +import type { Meta, StoryObj } from "@storybook/react"; + +const meta: Meta = { + title: "Example/IconButton", + component: IconButton, + parameters: { + layout: "centered", + }, + tags: ["!autodocs"], +}; + +export default meta; + +export const Primary: StoryObj = { + args: { + text: "갤러리", + iconName: "gallery", + size: "md", + }, +}; + +export const IconNameProps: StoryObj = { + render: () => ( +
+
+

gallery

+ +
+
+

camera

+ +
+
+ ), +}; + +export const SizeProps: StoryObj = { + render: () => ( +
+
+

sm

+ +
+
+ ), +}; diff --git a/src/components/ui/IconButton/IconButton.tsx b/src/components/ui/IconButton/IconButton.tsx new file mode 100644 index 0000000..036a372 --- /dev/null +++ b/src/components/ui/IconButton/IconButton.tsx @@ -0,0 +1,50 @@ +import React, { useCallback } from "react"; + +import classNames from "classnames"; + +import BaseButton from "@/components/ui/BaseButton/BaseButton"; +import Icon from "@/components/ui/Icon/Icon"; +import styles from "@/components/ui/IconButton/IconButton.module.scss"; +import type { IconButtonProps } from "@/components/ui/IconButton/IconButton.types"; + +const IconButton = React.forwardRef( + ( + { + as = BaseButton, + className, + size = "md", + disabled = false, + onClick, + text, + iconName, + ...props + }, + ref, + ) => { + const Comp = as as typeof BaseButton; + + const handleClick = useCallback( + (e: React.MouseEvent) => { + if (!disabled) { + onClick?.(e); + } + }, + [onClick, disabled], + ); + + return ( + + + {text} + + ); + }, +); + +export default IconButton; diff --git a/src/components/ui/IconButton/IconButton.types.ts b/src/components/ui/IconButton/IconButton.types.ts new file mode 100644 index 0000000..ba761ac --- /dev/null +++ b/src/components/ui/IconButton/IconButton.types.ts @@ -0,0 +1,11 @@ +import type { ButtonOwnProps } from "@/components/ui/BaseButton/BaseButton.types"; +import type { IconNameType } from "@/components/ui/Icon/Icon"; + +type IconButtonSize = "md" | "sm"; + +export interface IconButtonProps + extends React.ButtonHTMLAttributes, + ButtonOwnProps { + size?: IconButtonSize; + iconName: IconNameType; +} diff --git a/src/main.tsx b/src/main.tsx index 0f2457d..40b99a9 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -8,6 +8,7 @@ import App from "@/App"; import ReactQueryClientProvider from "@/components/provider/ReactQueryClientProvider"; import "@/styles/reset.scss"; +import "@/styles/global.scss"; createRoot(document.getElementById("root")!).render( diff --git a/src/styles/_mixins.scss b/src/styles/_mixins.scss index 21419b3..b06644d 100644 --- a/src/styles/_mixins.scss +++ b/src/styles/_mixins.scss @@ -33,17 +33,17 @@ font-weight: var(--font-weight-medium); } -@mixin buttonMd { +@mixin buttonPrimary { font-size: var(--font-size-default); font-weight: var(--font-weight-semi-bold); } -@mixin buttonSm { +@mixin buttonSecondary { font-size: var(--font-size-default); font-weight: var(--font-weight-medium); } -@mixin buttonXs { +@mixin buttonTertiary { font-size: var(--font-size-xs); font-weight: var(--font-weight-medium); } diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index f714bcd..0b0e75d 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -4,13 +4,13 @@ --font-weight-semi-bold: 600; --font-weight-bold: 700; - --font-size-xl: 28px; - --font-size-lg: 24px; - --font-size-md: 22px; - --font-size-default: 16px; - --font-size-sm: 15px; - --font-size-xs: 14px; - --font-size-xxs: 13px; + --font-size-xl: 1.75rem; + --font-size-lg: 1.5rem; + --font-size-md: 1.375rem; + --font-size-default: 1rem; + --font-size-sm: 0.9375rem; + --font-size-xs: 0.875rem; + --font-size-xxs: 0.8125rem; --color-white: #fff; --color-black: #000; @@ -22,6 +22,7 @@ --color-gray100: #f8f8f8; --color-gray200: #ebecf0; --color-gray300: #e1e2e8; + --color-gray350: #dcdde3; --color-gray400: #00000026; --color-gray500: #00000040; --color-gray600: #363642; diff --git a/src/styles/global.scss b/src/styles/global.scss new file mode 100644 index 0000000..0e78698 --- /dev/null +++ b/src/styles/global.scss @@ -0,0 +1,29 @@ +* { + font-family: "Pretendard", sans-serif; + padding: 0; + margin: 0; + box-sizing: border-box; +} + +body { + max-width: 37.5rem; + margin: 0 auto; +} + +a { + text-decoration: none; + color: inherit; +} + +button { + border: none; + outline: none; + background-color: transparent; +} + +input, +textarea { + border: none; + outline: none; + background-color: transparent; +} diff --git a/src/styles/reset.scss b/src/styles/reset.scss index a04e60e..2560287 100644 --- a/src/styles/reset.scss +++ b/src/styles/reset.scss @@ -128,7 +128,3 @@ table { border-collapse: collapse; border-spacing: 0; } - -* { - font-family: "Pretendard", sans-serif; -} diff --git a/tsconfig.json b/tsconfig.json index ed6beba..0b035c7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,5 +27,5 @@ "plugins": [{ "name": "typescript-plugin-css-modules" }], "types": ["@testing-library/jest-dom", "vite-plugin-svgr/client", "vite/client"] }, - "include": ["src", "src/types"] + "include": ["src", "src/types", ".storybook/**/*"] } diff --git a/vite.config.ts b/vite.config.ts index efc2bd5..7eafd3f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,15 +11,18 @@ export default defineConfig({ base: "/", resolve: { alias: { - "@": path.resolve(__dirname, "./src"), + "@": path.resolve(__dirname, "src"), }, }, css: { + modules: { + localsConvention: "camelCase", + }, preprocessorOptions: { scss: { additionalData: ` - @use "@/styles/_variables.scss" as *; - @use "@/styles/_mixins.scss" as *; + @use "@/styles/_variables.scss"; + @use "@/styles/_mixins.scss"; `, }, },