Skip to content

Commit bddcf51

Browse files
refactor: JDS 버튼 컴포넌트 vanilla-extract 마이그레이션 (#432)
* refactor: BlockButton VE 마이그레이션 * refactor: LabelButton VE 마이그레이션 * chore: @vanilla-extract/dynamic 패키지 의존성 추가 * refactor: 스토리북 레이아웃 컴포넌트 VE 마이그레이션 * fix: BlockButtonFeedbackProps intent를 optional로 수정 * refactor: 버튼 타이포 토큰을 vars.typo.primitive 참조로 교체 * refactor: 버튼 recipe defaultVariants 제거 * refactor: 버튼 variant 옵션 상수 분리 및 SSOT 적용 * refactor: LabelButton inset을 pxToRem으로 교체 * chore: focusRing, forwardRef, overlay 유틸 export 추가 * refactor: 버튼 컴포넌트 utils import 경로 단축 * fix: LabelButtonFeedbackProps intent를 optional로 수정 * refactor: 버튼 컴포넌트 data-part prop 외부 주입 제한
1 parent 9f572bf commit bddcf51

17 files changed

Lines changed: 837 additions & 1498 deletions

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { createVar, style } from "@vanilla-extract/css";
2+
3+
export const gapVar = createVar();
4+
5+
export const flexRow = style({
6+
display: "flex",
7+
flexDirection: "row",
8+
alignItems: "center",
9+
gap: gapVar,
10+
});
11+
12+
export const flexColumn = style({
13+
display: "flex",
14+
flexDirection: "column",
15+
gap: gapVar,
16+
});
17+
18+
export const label = style({
19+
width: "100px",
20+
});
Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,38 @@
1-
import styled from "@emotion/styled";
1+
import { assignInlineVars } from "@vanilla-extract/dynamic";
2+
import type { ComponentPropsWithoutRef } from "react";
23

3-
/**
4-
* Storybook 전용 레이아웃 컴포넌트
5-
* Stories 파일에서 반복되는 레이아웃 스타일을 재사용하기 위한 유틸리티
6-
*/
4+
import { flexColumn, flexRow, gapVar, label } from "./layout.css";
75

8-
export const FlexRow = styled.div<{ gap?: string }>(({ gap = "16px" }) => ({
9-
display: "flex",
10-
flexDirection: "row",
11-
gap,
12-
alignItems: "center",
13-
}));
6+
type WithGap = { gap?: string };
147

15-
export const FlexColumn = styled.div<{ gap?: string }>(({ gap = "24px" }) => ({
16-
display: "flex",
17-
flexDirection: "column",
18-
gap,
19-
}));
8+
export function FlexRow({
9+
gap = "16px",
10+
style,
11+
...props
12+
}: ComponentPropsWithoutRef<"div"> & WithGap) {
13+
return (
14+
<div
15+
className={flexRow}
16+
style={{ ...assignInlineVars({ [gapVar]: gap }), ...style }}
17+
{...props}
18+
/>
19+
);
20+
}
2021

21-
export const Label = styled.span({
22-
width: "100px",
23-
});
22+
export function FlexColumn({
23+
gap = "24px",
24+
style,
25+
...props
26+
}: ComponentPropsWithoutRef<"div"> & WithGap) {
27+
return (
28+
<div
29+
className={flexColumn}
30+
style={{ ...assignInlineVars({ [gapVar]: gap }), ...style }}
31+
{...props}
32+
/>
33+
);
34+
}
35+
36+
export function Label({ ...props }: ComponentPropsWithoutRef<"span">) {
37+
return <span className={label} {...props} />;
38+
}

packages/jds/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
"@types/react": "^18.3.18",
8989
"@types/react-dom": "^18.3.5",
9090
"@vanilla-extract/css": "^1.20.1",
91+
"@vanilla-extract/dynamic": "^2.1.5",
9192
"@vanilla-extract/esbuild-plugin": "^2.3.22",
9293
"@vanilla-extract/recipes": "^0.5.7",
9394
"@vanilla-extract/vite-plugin": "^5.2.2",

packages/jds/src/components/Button/BlockButton/BlockButton.stories.tsx

Lines changed: 23 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { Meta, StoryObj } from "@storybook/react-vite";
22
import { FlexRow, FlexColumn, Label } from "@storybook-utils/layout";
3-
import { BlockButton } from "components";
3+
4+
import { BlockButton } from "./BlockButton";
5+
import { BLOCK_BUTTON_HIERARCHY_OPTIONS, BLOCK_BUTTON_STYLE_OPTIONS } from "./blockButton.types";
46

57
const meta = {
68
title: "Components/BlockButton",
@@ -18,7 +20,7 @@ const meta = {
1820
},
1921
hierarchy: {
2022
control: "select",
21-
options: ["accent", "primary", "secondary", "tertiary"],
23+
options: BLOCK_BUTTON_HIERARCHY_OPTIONS,
2224
description: "버튼의 시각적 위계",
2325
table: {
2426
defaultValue: { summary: "primary" },
@@ -149,11 +151,11 @@ export const InteractionStates: Story = {
149151
docs: {
150152
description: {
151153
story:
152-
"InteractionLayer 기반 인터랙션 시스템:\n\n" +
154+
"VE overlay 유틸 기반 인터랙션 시스템 (data attribute 방식):\n\n" +
153155
"- **rest**: 기본 상태 (opacity: 0)\n" +
154-
"- **hover**: 마우스 오버 시 (opacity: 0.08, fluent motion 100ms)\n" +
155-
"- **active**: 클릭 중 (opacity: 0.12, transition 없음)\n" +
156-
"- **focus**: 키보드 포커스 시 (focus outline 표시, transition 없음)",
156+
"- **data-hovered**: 마우스 오버 시 (opacity: 0.08, fluent motion 100ms)\n" +
157+
"- **data-pressed**: 클릭 중 (opacity: 0.12, transition 없음)\n" +
158+
"- **data-focus-visible**: 키보드 포커스 시 (focus ring 표시)",
157159
},
158160
},
159161
},
@@ -165,13 +167,11 @@ export const ComprehensiveMatrix: Story = {
165167
},
166168
render: () => (
167169
<FlexColumn gap='32px'>
168-
{(["solid", "outlined", "empty"] as const).map(variant => (
170+
{BLOCK_BUTTON_STYLE_OPTIONS.map(variant => (
169171
<FlexColumn key={variant} gap='12px'>
170-
<h3 style={{ margin: 0, fontSize: "14px", fontWeight: "bold" }}>
171-
{variant.charAt(0).toUpperCase() + variant.slice(1)}
172-
</h3>
172+
<Label>{variant.charAt(0).toUpperCase() + variant.slice(1)}</Label>
173173
<FlexRow gap='12px'>
174-
{(["accent", "primary", "secondary", "tertiary"] as const).map(hierarchy => (
174+
{BLOCK_BUTTON_HIERARCHY_OPTIONS.map(hierarchy => (
175175
<BlockButton.Basic key={hierarchy} variant={variant} hierarchy={hierarchy}>
176176
{hierarchy}
177177
</BlockButton.Basic>
@@ -222,36 +222,18 @@ export const FeedbackButtons: Story = {
222222
},
223223
render: () => (
224224
<FlexColumn>
225-
<Label>Positive:</Label>
226-
<FlexRow gap='12px'>
227-
<BlockButton.Feedback intent='positive' size='xs'>
228-
저장
229-
</BlockButton.Feedback>
230-
<BlockButton.Feedback intent='positive' size='sm'>
231-
저장
232-
</BlockButton.Feedback>
233-
<BlockButton.Feedback intent='positive' size='md'>
234-
저장
235-
</BlockButton.Feedback>
236-
<BlockButton.Feedback intent='positive' size='lg'>
237-
저장
238-
</BlockButton.Feedback>
239-
</FlexRow>
240-
<Label>Destructive:</Label>
241-
<FlexRow gap='12px'>
242-
<BlockButton.Feedback intent='destructive' size='xs'>
243-
삭제
244-
</BlockButton.Feedback>
245-
<BlockButton.Feedback intent='destructive' size='sm'>
246-
삭제
247-
</BlockButton.Feedback>
248-
<BlockButton.Feedback intent='destructive' size='md'>
249-
삭제
250-
</BlockButton.Feedback>
251-
<BlockButton.Feedback intent='destructive' size='lg'>
252-
삭제
253-
</BlockButton.Feedback>
254-
</FlexRow>
225+
{(["positive", "destructive"] as const).map(intent => (
226+
<FlexColumn key={intent} gap='12px'>
227+
<Label>{intent.charAt(0).toUpperCase() + intent.slice(1)}:</Label>
228+
<FlexRow gap='12px'>
229+
{(["xs", "sm", "md", "lg"] as const).map(size => (
230+
<BlockButton.Feedback key={size} intent={intent} size={size}>
231+
{intent === "positive" ? "저장" : "삭제"}
232+
</BlockButton.Feedback>
233+
))}
234+
</FlexRow>
235+
</FlexColumn>
236+
))}
255237
</FlexColumn>
256238
),
257239
parameters: {

packages/jds/src/components/Button/BlockButton/BlockButton.tsx

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import { mergeProps } from "@react-aria/utils";
2+
import { clsx } from "clsx";
13
import type { BlockButtonBasicProps, BlockButtonFeedbackProps } from "components";
24
import { Icon } from "components";
5+
import { usePressable } from "hooks";
36
import { forwardRef } from "react";
47

5-
import { iconSizeMap, StyledBlockButton } from "./blockButton.styles";
8+
import { basicRoot, feedbackRoot, iconSizeMap } from "./blockButton.css";
69

710
const BlockButtonBasic = forwardRef<HTMLButtonElement, BlockButtonBasicProps>(
811
(
@@ -14,27 +17,25 @@ const BlockButtonBasic = forwardRef<HTMLButtonElement, BlockButtonBasicProps>(
1417
prefixIcon,
1518
suffixIcon,
1619
disabled = false,
20+
className,
1721
...restProps
1822
},
19-
ref,
23+
forwardedRef,
2024
) => {
21-
//Todo: 아이콘 사이즈도 전부 스타일의 theme 단위에서 해결하면 좋을듯(Theme 구조 추가 필요)
25+
const { ref, pressableProps } = usePressable(forwardedRef, { disabled });
2226
const iconSize = iconSizeMap[size];
2327

2428
return (
25-
<StyledBlockButton
29+
<button
2630
ref={ref}
27-
$hierarchy={hierarchy}
28-
$variant={variant}
29-
$size={size}
30-
$disabled={disabled}
31-
disabled={disabled}
32-
{...restProps}
31+
{...mergeProps(pressableProps, restProps)}
32+
data-part='root'
33+
className={clsx(basicRoot({ hierarchy, variant, size }), className)}
3334
>
3435
{prefixIcon && <Icon name={prefixIcon} size={iconSize} />}
3536
{children}
3637
{suffixIcon && <Icon name={suffixIcon} size={iconSize} />}
37-
</StyledBlockButton>
38+
</button>
3839
);
3940
},
4041
);
@@ -50,25 +51,25 @@ const BlockButtonFeedback = forwardRef<HTMLButtonElement, BlockButtonFeedbackPro
5051
prefixIcon,
5152
suffixIcon,
5253
disabled = false,
54+
className,
5355
...restProps
5456
},
55-
ref,
57+
forwardedRef,
5658
) => {
59+
const { ref, pressableProps } = usePressable(forwardedRef, { disabled });
5760
const iconSize = iconSizeMap[size];
5861

5962
return (
60-
<StyledBlockButton
63+
<button
6164
ref={ref}
62-
$intent={intent}
63-
$size={size}
64-
$disabled={disabled}
65-
disabled={disabled}
66-
{...restProps}
65+
{...mergeProps(pressableProps, restProps)}
66+
data-part='root'
67+
className={clsx(feedbackRoot({ intent, size }), className)}
6768
>
6869
{prefixIcon && <Icon name={prefixIcon} size={iconSize} />}
6970
{children}
7071
{suffixIcon && <Icon name={suffixIcon} size={iconSize} />}
71-
</StyledBlockButton>
72+
</button>
7273
);
7374
},
7475
);

0 commit comments

Comments
 (0)