diff --git a/.changeset/.grumpy-llamas-occur.md b/.changeset/.grumpy-llamas-occur.md new file mode 100644 index 00000000..cad8d70f --- /dev/null +++ b/.changeset/.grumpy-llamas-occur.md @@ -0,0 +1,5 @@ +--- +"wowds-ui": patch +--- + +Stepper UI를 배포합니다. diff --git a/packages/wow-ui/package.json b/packages/wow-ui/package.json index 1e070f17..4e6ddf1a 100644 --- a/packages/wow-ui/package.json +++ b/packages/wow-ui/package.json @@ -30,6 +30,11 @@ "require": "./dist/Switch.cjs", "import": "./dist/Switch.js" }, + "./Stepper": { + "types": "./dist/components/Stepper/index.d.ts", + "require": "./dist/Stepper.cjs", + "import": "./dist/Stepper.js" + }, "./RadioButton": { "types": "./dist/components/RadioGroup/RadioButton.d.ts", "require": "./dist/RadioButton.cjs", diff --git a/packages/wow-ui/rollup.config.js b/packages/wow-ui/rollup.config.js index 6763c2cb..948ec1e8 100644 --- a/packages/wow-ui/rollup.config.js +++ b/packages/wow-ui/rollup.config.js @@ -22,6 +22,7 @@ export default { input: { TextField: "./src/components/TextField", Switch: "./src/components/Switch", + Stepper: "./src/components/Stepper", RadioButton: "./src/components/RadioGroup/RadioButton", RadioGroup: "./src/components/RadioGroup/RadioGroup", MultiGroup: "./src/components/MultiGroup", diff --git a/packages/wow-ui/src/components/Stepper/Stepper.stories.tsx b/packages/wow-ui/src/components/Stepper/Stepper.stories.tsx new file mode 100644 index 00000000..498ac9f1 --- /dev/null +++ b/packages/wow-ui/src/components/Stepper/Stepper.stories.tsx @@ -0,0 +1,76 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import Stepper from "@/components/Stepper"; + +const meta = { + title: "UI/Stepper", + component: Stepper, + parameters: { + componentSubtitle: "스텝퍼 컴포넌트", + a11y: { + config: { + rules: [{ id: "color-contrast", enabled: false }], + }, + }, + }, + tags: ["autodocs"], + argTypes: { + step: { + description: "프로그래스 바의 현재 스텝을 나타냅니다.", + table: { + type: { summary: "number" }, + }, + control: { + type: "number", + }, + }, + labels: { + description: "프로그래스 바 하단에 나타낼 라벨의 배열을 나타냅니다.", + table: { + type: { summary: "LabelType[]" }, + }, + control: false, + }, + maxStep: { + description: "프로그래스 바가 가질 수 있는 최대 스텝을 나타냅니다.", + table: { + type: { summary: "number" }, + }, + control: { + type: "number", + }, + }, + width: { + description: "프로그래스 바의 너비를 지정합니다.", + table: { + type: { summary: "number" }, + }, + control: { + type: "text", + }, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + step: 1, + maxStep: 5, + }, +}; + +export const StepperWithMarkers: Story = { + args: { + step: 1, + maxStep: 3, + labels: [ + { value: 1, label: "Label" }, + { value: 2, label: "Label" }, + { value: 3, label: "Label" }, + ], + }, +}; diff --git a/packages/wow-ui/src/components/Stepper/index.tsx b/packages/wow-ui/src/components/Stepper/index.tsx new file mode 100644 index 00000000..c05525cb --- /dev/null +++ b/packages/wow-ui/src/components/Stepper/index.tsx @@ -0,0 +1,202 @@ +import { cva } from "@styled-system/css"; +import { styled } from "@styled-system/jsx"; +import type { ReactNode } from "react"; +import { useCallback, useMemo } from "react"; + +import { calcPercent } from "@/utils/calcPercent"; + +export interface StepperProps { + step: number; + maxStep?: number; + labels?: LabelType[]; + width?: number; +} + +export type LabelType = { + value: number; + label: ReactNode; +}; + +const checkStepperStatus = (number: number, step: number) => { + if (step === number) return "currentStep"; + if (step > number) return "checkedStep"; + return "default"; +}; + +/** + * @param {number} step Stepper의 현재 스텝 + * @param {number} [maxStep] Stepper가 가질 수 있는 최대 스텝 + * @param {LabelType[]} [labels] Stepper에 하단에 입력할 라벨의 배열 + * @param {number} width Stepper의 가로 길이를 자유롭게 정할 수 있어요. 단, 278px 이상이어야 합니다. + */ + +const Stepper = ({ step, maxStep = 3, labels, width }: StepperProps) => { + const fillStepper = useCallback((maxStep: number, step: number) => { + const ratio = (step - 1) / (maxStep - 1); + return ratio > 1 ? "100%" : `${ratio * 100}%`; + }, []); + + const circleNumbers = useMemo( + () => Array.from({ length: maxStep }, (_, i) => i + 1), + [maxStep] + ); + + return ( +
+ 278 ? `${width}px` : "17.375rem" }} + > + + + {circleNumbers.map((circleNumber) => ( + + ))} + + {labels && ( + + {labels.map((label) => ( + + ))} + + )} + +
+ ); +}; + +export default Stepper; + +const StepperCircle = ({ + maxStep, + circleNumber, + currentStep, +}: { + maxStep: number; + circleNumber: number; + currentStep: number; +}) => { + return ( + + {circleNumber} + + ); +}; + +const stepperCircleStyle = cva({ + base: { + textStyle: "label2", + alignItems: "center", + borderRadius: "full", + display: "flex", + height: "1.5rem", + justifyContent: "center", + pointerEvents: "none", + position: "absolute", + width: "1.5rem", + borderWidth: "1px", + transform: "translateX(-50%)", + }, + variants: { + status: { + default: { + borderWidth: "0.0625rem", + borderColor: "outline", + backgroundColor: "backgroundNormal", + color: "sub", + }, + checkedStep: { + borderWidth: "0.0625rem", + borderColor: "primary", + color: "primary", + backgroundColor: "backgroundNormal", + }, + currentStep: { + backgroundColor: "primary", + color: "textWhite", + }, + }, + }, +}); + +const StepperLabel = ({ + labelObject, + maxStep, + currentStep, +}: { + labelObject: LabelType; + maxStep: number; + currentStep: number; +}) => { + const { value, label } = labelObject; + + return ( + + + {label} + + + ); +}; + +const stepperLabelStyle = cva({ + base: { + textStyle: "label2", + }, + variants: { + status: { + default: { + color: "sub", + }, + checkedStep: { + color: "primary", + }, + currentStep: { + color: "textBlack", + }, + }, + }, +}); diff --git a/packages/wow-ui/src/utils/calcPercent.ts b/packages/wow-ui/src/utils/calcPercent.ts new file mode 100644 index 00000000..e0db8d1f --- /dev/null +++ b/packages/wow-ui/src/utils/calcPercent.ts @@ -0,0 +1,3 @@ +export const calcPercent = (maxValue: number, value: number) => { + return (value / (maxValue - 1)) * 100; +};