Skip to content

Commit fc5ee9d

Browse files
authored
Feat(design-system): progress bar 구현 (#25)
* feat: 프로그래스바 구현및 스토리북 작성 * feat: 프로필 카드 디자인 커스텀 * feat: tree버전 커스텀 * feat: 스토리북 수정 * feat: 주석 제거 * feat: progress 컴포넌트 수정 및 코드 정리 * feat: Progress 컴포넌트 추가 및 대문자 수정 * refator: 파일 이름 변경 * refactor: 파일 이름 변경
1 parent 49c3959 commit fc5ee9d

File tree

6 files changed

+229
-2
lines changed

6 files changed

+229
-2
lines changed

packages/design-system/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
},
5555
"dependencies": {
5656
"@pivanov/vite-plugin-svg-sprite": "^3.1.3",
57+
"@radix-ui/react-progress": "^1.1.7",
5758
"@radix-ui/react-slot": "^1.2.3",
5859
"@radix-ui/react-switch": "^1.2.6",
5960
"class-variance-authority": "^0.7.1",
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { default as Button } from './button/Button';
2-
export { Switch } from './switch/switch';
2+
export { Switch } from './switch/Switch';
33
export { default as Input } from './input/Input';
44
export { Textarea } from './textarea/Textarea';
5+
export { Progress } from './progress/Progress';
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import type { Meta, StoryObj } from '@storybook/react-vite';
2+
import React, { useEffect, useState } from 'react';
3+
import { within, userEvent, expect } from '@storybook/test';
4+
import { Progress } from './Progress';
5+
6+
const meta: Meta<typeof Progress> = {
7+
title: 'Components/Progress',
8+
component: Progress,
9+
tags: ['autodocs'],
10+
parameters: {
11+
layout: 'centered',
12+
docs: {
13+
description: {
14+
component:
15+
'단일 로직을 공유하고 **variant**로 스타일만 분기하는 progress입니다.\n' +
16+
'- `variant="profile"`: 얇은 트랙 + 단색 인디케이터 (기본)\n' +
17+
'- `variant="tree"`: 두꺼운 트랙 + 그라데이션 인디케이터\n',
18+
},
19+
},
20+
},
21+
argTypes: {
22+
className: { table: { disable: true } },
23+
asChild: { table: { disable: true } },
24+
ref: { table: { disable: true } },
25+
value: {
26+
control: { type: 'range', min: 0, max: 100, step: 1 },
27+
description: '진행 퍼센트(0–100)',
28+
},
29+
variant: {
30+
control: { type: 'radio' },
31+
options: ['profile', 'tree'],
32+
description: '스타일 분기',
33+
},
34+
},
35+
args: {
36+
value: 40,
37+
variant: 'profile',
38+
},
39+
};
40+
41+
export default meta;
42+
type Story = StoryObj<typeof Progress>;
43+
44+
const Frame: React.FC<React.ComponentProps<typeof Progress>> = (props) => (
45+
<div style={{ width: 320 }}>
46+
<Progress {...props} />
47+
</div>
48+
);
49+
50+
export const Profile: Story = {
51+
args: { variant: 'profile', value: 70 },
52+
render: (args) => <Frame {...args} />,
53+
};
54+
55+
export const Tree: Story = {
56+
args: { variant: 'tree', value: 70 },
57+
render: (args) => <Frame {...args} />,
58+
};
59+
60+
export const Zero: Story = {
61+
args: { value: 0 },
62+
render: (args) => <Frame {...args} />,
63+
};
64+
65+
export const Full: Story = {
66+
args: { value: 100 },
67+
render: (args) => <Frame {...args} />,
68+
};
69+
70+
const AutoProgressDemo: React.FC<{ variant?: 'profile' | 'tree' }> = ({
71+
variant = 'profile',
72+
}) => {
73+
const [v, setV] = useState(0);
74+
useEffect(() => {
75+
const id = setInterval(() => setV((p) => (p >= 100 ? 0 : p + 10)), 250);
76+
return () => clearInterval(id);
77+
}, []);
78+
return <Frame value={v} variant={variant} />;
79+
};
80+
81+
export const AutoProgressBoth: StoryObj<typeof Progress> = {
82+
name: 'Auto progress — profile & tree',
83+
parameters: {
84+
controls: { exclude: ['value', 'variant'] },
85+
layout: 'centered',
86+
},
87+
render: () => (
88+
<div style={{ display: 'flex', gap: 16, width: 760 }}>
89+
<div>
90+
<div style={{ fontSize: 14, opacity: 0.7, marginBottom: 8 }}>
91+
profile
92+
</div>
93+
<AutoProgressDemo variant="profile" />
94+
</div>
95+
<div>
96+
<div style={{ fontSize: 14, opacity: 0.7, marginBottom: 8 }}>tree</div>
97+
<AutoProgressDemo variant="tree" />
98+
</div>
99+
</div>
100+
),
101+
};
102+
const ClickToAdvanceSync: React.FC = () => {
103+
const [v, setV] = React.useState(0);
104+
return (
105+
<div style={{ width: 320, display: 'grid', gap: 12 }}>
106+
<Progress value={v} variant="profile" />
107+
<Progress value={v} variant="tree" />
108+
<div style={{ display: 'flex', gap: 8 }}>
109+
<button
110+
type="button"
111+
onClick={() => setV((p) => Math.min(p + 20, 100))}
112+
>
113+
+20% (both)
114+
</button>
115+
<button type="button" onClick={() => setV(0)}>
116+
Reset
117+
</button>
118+
<span style={{ marginLeft: 'auto', fontSize: 12, opacity: 0.7 }}>
119+
{v}%
120+
</span>
121+
</div>
122+
</div>
123+
);
124+
};
125+
126+
export const WithInteractionSync: Story = {
127+
name: 'With interaction — sync both',
128+
render: () => <ClickToAdvanceSync />,
129+
play: async ({ canvasElement }) => {
130+
const canvas = within(canvasElement);
131+
const btn = await canvas.findByRole('button', { name: /\+20% \(both\)/i });
132+
await userEvent.click(btn);
133+
await userEvent.click(btn);
134+
135+
const roots = canvasElement.querySelectorAll('[data-slot="progress"]');
136+
roots.forEach((el) => expect(el.getAttribute('aria-valuenow')).toBe('40'));
137+
},
138+
};
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import * as React from 'react';
2+
import * as ProgressPrimitive from '@radix-ui/react-progress';
3+
import { cva, type VariantProps } from 'class-variance-authority';
4+
import { cn } from '@/lib/utils';
5+
6+
const track = cva('relative w-full overflow-hidden rounded-full', {
7+
variants: {
8+
variant: {
9+
profile: 'h-[0.4rem] bg-gray100',
10+
tree: 'h-[1.2rem] bg-gray100',
11+
},
12+
},
13+
defaultVariants: { variant: 'profile' },
14+
});
15+
16+
const indicator = cva(
17+
'h-full rounded-full transition-[width] duration-300 ease-out',
18+
{
19+
variants: {
20+
variant: {
21+
profile: 'bg-main400',
22+
tree: 'bg-gradient-to-r from-gradient-start to-gradient-end',
23+
},
24+
},
25+
defaultVariants: { variant: 'profile' },
26+
}
27+
);
28+
29+
export interface ProgressProps
30+
extends Omit<
31+
React.ComponentProps<typeof ProgressPrimitive.Root>,
32+
'value' | 'max'
33+
>,
34+
VariantProps<typeof track> {
35+
value: number;
36+
}
37+
38+
export function Progress({
39+
className,
40+
variant,
41+
value,
42+
...props
43+
}: ProgressProps) {
44+
const progressPercent = Math.max(0, Math.min(100, value));
45+
46+
return (
47+
<ProgressPrimitive.Root
48+
data-slot="progress"
49+
className={cn(track({ variant }), className)}
50+
value={progressPercent}
51+
max={100}
52+
{...props}
53+
>
54+
<ProgressPrimitive.Indicator
55+
data-slot="progress-indicator"
56+
className={indicator({ variant })}
57+
style={{ width: `${progressPercent}%` }}
58+
/>
59+
</ProgressPrimitive.Root>
60+
);
61+
}

packages/design-system/src/components/switch/switch.tsx renamed to packages/design-system/src/components/switch/Switch.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ function Switch({
1111
<SwitchPrimitive.Root
1212
data-slot="switch"
1313
className={cn(
14-
'data-[state=checked]:bg-main400 data-[state=unchecked]:bg-gray200 focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 shadow-xs peer inline-flex h-[1.15rem] h-[2rem] w-[4rem] shrink-0 items-center rounded-full border border-transparent outline-none transition-all focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
14+
'data-[state=checked]:bg-main400 data-[state=unchecked]:bg-gray200 focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 shadow-xs peer inline-flex h-[2rem] w-[4rem] shrink-0 items-center rounded-full border border-transparent outline-none transition-all focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
1515
className
1616
)}
1717
{...props}

pnpm-lock.yaml

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)