teachable-design-system 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +69 -0
  2. package/dist/index.cjs.js +3515 -0
  3. package/dist/index.cjs.js.map +1 -0
  4. package/dist/index.esm.js +3494 -0
  5. package/dist/index.esm.js.map +1 -0
  6. package/dist/src/assets/index.d.ts +2 -0
  7. package/dist/src/assets/index.d.ts.map +1 -0
  8. package/dist/src/components/Button/Button.d.ts +4 -0
  9. package/dist/src/components/Button/Button.d.ts.map +1 -0
  10. package/dist/src/components/Button/index.d.ts +2 -0
  11. package/dist/src/components/Button/index.d.ts.map +1 -0
  12. package/dist/src/components/Button/style.d.ts +13 -0
  13. package/dist/src/components/Button/style.d.ts.map +1 -0
  14. package/dist/src/components/CheckBox/CheckBox.d.ts +4 -0
  15. package/dist/src/components/CheckBox/CheckBox.d.ts.map +1 -0
  16. package/dist/src/components/CheckBox/index.d.ts +2 -0
  17. package/dist/src/components/CheckBox/index.d.ts.map +1 -0
  18. package/dist/src/components/CheckBox/style.d.ts +22 -0
  19. package/dist/src/components/CheckBox/style.d.ts.map +1 -0
  20. package/dist/src/components/Dropdown/Dropdown.d.ts +3 -0
  21. package/dist/src/components/Dropdown/Dropdown.d.ts.map +1 -0
  22. package/dist/src/components/Dropdown/index.d.ts +2 -0
  23. package/dist/src/components/Dropdown/index.d.ts.map +1 -0
  24. package/dist/src/components/Dropdown/style.d.ts +35 -0
  25. package/dist/src/components/Dropdown/style.d.ts.map +1 -0
  26. package/dist/src/components/Input/Input.d.ts +4 -0
  27. package/dist/src/components/Input/Input.d.ts.map +1 -0
  28. package/dist/src/components/Input/index.d.ts +2 -0
  29. package/dist/src/components/Input/index.d.ts.map +1 -0
  30. package/dist/src/components/Input/style.d.ts +5 -0
  31. package/dist/src/components/Input/style.d.ts.map +1 -0
  32. package/dist/src/components/Sidebar/Sidebar.d.ts +3 -0
  33. package/dist/src/components/Sidebar/Sidebar.d.ts.map +1 -0
  34. package/dist/src/components/Sidebar/index.d.ts +2 -0
  35. package/dist/src/components/Sidebar/index.d.ts.map +1 -0
  36. package/dist/src/components/Sidebar/style.d.ts +37 -0
  37. package/dist/src/components/Sidebar/style.d.ts.map +1 -0
  38. package/dist/src/components/TabBar/TabBar.d.ts +13 -0
  39. package/dist/src/components/TabBar/TabBar.d.ts.map +1 -0
  40. package/dist/src/components/TabBar/index.d.ts +2 -0
  41. package/dist/src/components/TabBar/index.d.ts.map +1 -0
  42. package/dist/src/components/TabBar/style.d.ts +21 -0
  43. package/dist/src/components/TabBar/style.d.ts.map +1 -0
  44. package/dist/src/components/index.d.ts +7 -0
  45. package/dist/src/components/index.d.ts.map +1 -0
  46. package/dist/src/foundation/index.d.ts +2 -0
  47. package/dist/src/foundation/index.d.ts.map +1 -0
  48. package/dist/src/index.d.ts +3 -0
  49. package/dist/src/index.d.ts.map +1 -0
  50. package/dist/src/style/index.d.ts +2 -0
  51. package/dist/src/style/index.d.ts.map +1 -0
  52. package/dist/src/style/theme/colors.d.ts +474 -0
  53. package/dist/src/style/theme/colors.d.ts.map +1 -0
  54. package/dist/src/style/theme/index.d.ts +3 -0
  55. package/dist/src/style/theme/index.d.ts.map +1 -0
  56. package/dist/src/style/theme/typography.d.ts +181 -0
  57. package/dist/src/style/theme/typography.d.ts.map +1 -0
  58. package/dist/src/type/Dropdown.types.d.ts +8 -0
  59. package/dist/src/type/Dropdown.types.d.ts.map +1 -0
  60. package/dist/src/type/Sidebar.types.d.ts +6 -0
  61. package/dist/src/type/Sidebar.types.d.ts.map +1 -0
  62. package/dist/src/types/button.types.d.ts +12 -0
  63. package/dist/src/types/button.types.d.ts.map +1 -0
  64. package/dist/src/types/checkBox.types.d.ts +7 -0
  65. package/dist/src/types/checkBox.types.d.ts.map +1 -0
  66. package/dist/src/types/input.types.d.ts +11 -0
  67. package/dist/src/types/input.types.d.ts.map +1 -0
  68. package/dist/tsconfig.build.tsbuildinfo +1 -0
  69. package/package.json +60 -0
  70. package/src/assets/icons/arrow-down.png +0 -0
  71. package/src/assets/icons/checked.png +0 -0
  72. package/src/assets/icons/icon_size.png +0 -0
  73. package/src/assets/images/.gitkeep +0 -0
  74. package/src/assets/index.ts +1 -0
  75. package/src/components/Button/Button.stories.tsx +145 -0
  76. package/src/components/Button/Button.tsx +27 -0
  77. package/src/components/Button/index.ts +1 -0
  78. package/src/components/Button/style.ts +132 -0
  79. package/src/components/CheckBox/CheckBox.stories.tsx +203 -0
  80. package/src/components/CheckBox/CheckBox.tsx +34 -0
  81. package/src/components/CheckBox/index.ts +1 -0
  82. package/src/components/CheckBox/style.ts +76 -0
  83. package/src/components/Dropdown/Dropdown.stories.tsx +91 -0
  84. package/src/components/Dropdown/Dropdown.tsx +56 -0
  85. package/src/components/Dropdown/index.ts +1 -0
  86. package/src/components/Dropdown/style.ts +213 -0
  87. package/src/components/Input/Input.stories.tsx +152 -0
  88. package/src/components/Input/Input.tsx +33 -0
  89. package/src/components/Input/index.ts +1 -0
  90. package/src/components/Input/style.ts +88 -0
  91. package/src/components/Sidebar/Sidebar.stories.tsx +60 -0
  92. package/src/components/Sidebar/Sidebar.tsx +67 -0
  93. package/src/components/Sidebar/index.ts +1 -0
  94. package/src/components/Sidebar/index.tsx +1 -0
  95. package/src/components/Sidebar/style.ts +109 -0
  96. package/src/components/TabBar/TabBar.stories.tsx +116 -0
  97. package/src/components/TabBar/TabBar.tsx +47 -0
  98. package/src/components/TabBar/index.ts +1 -0
  99. package/src/components/TabBar/style.ts +53 -0
  100. package/src/components/index.ts +6 -0
  101. package/src/foundation/index.ts +1 -0
  102. package/src/index.ts +2 -0
  103. package/src/style/index.ts +1 -0
  104. package/src/style/theme/colors.ts +504 -0
  105. package/src/style/theme/index.ts +2 -0
  106. package/src/style/theme/typography.ts +194 -0
  107. package/src/type/Dropdown.types.ts +7 -0
  108. package/src/type/Sidebar.types.ts +5 -0
  109. package/src/types/ krds-uiux.d.ts +12 -0
  110. package/src/types/button.types.ts +12 -0
  111. package/src/types/checkBox.types.ts +6 -0
  112. package/src/types/input.types.ts +11 -0
  113. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,152 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import Input from './Input';
3
+ import React, { useState } from 'react';
4
+ import { InputProps } from '../../types/input.types';
5
+
6
+ const meta: Meta<InputProps> = {
7
+ title: 'Components/Input',
8
+ component: Input,
9
+ parameters: {
10
+ layout: 'centered',
11
+ },
12
+ tags: ['autodocs'],
13
+ argTypes: {
14
+ size: {
15
+ control: 'select',
16
+ options: ['small', 'medium', 'large'],
17
+ description: '인풋의 크기',
18
+ },
19
+ label: {
20
+ control: 'boolean',
21
+ description: 'label 표시 여부',
22
+ },
23
+ labelText: {
24
+ control: 'text',
25
+ description: 'label에 표시될 텍스트',
26
+ },
27
+ placeholder: {
28
+ control: 'text',
29
+ description: 'placeholder 텍스트',
30
+ },
31
+ disabled: {
32
+ control: 'boolean',
33
+ description: '비활성화 여부',
34
+ },
35
+ },
36
+ } satisfies Meta<typeof Input>;
37
+
38
+ export default meta;
39
+ type Story = StoryObj<typeof meta>;
40
+
41
+ // 기본 스토리
42
+ export const Default: Story = {
43
+ args: {
44
+ size: 'medium',
45
+ label: true,
46
+ labelText: 'Label',
47
+ placeholder: 'Enter text...',
48
+ disabled: false,
49
+ },
50
+ };
51
+
52
+ // Small 사이즈
53
+ export const Small: Story = {
54
+ args: {
55
+ size: 'small',
56
+ label: true,
57
+ labelText: 'Small Input',
58
+ placeholder: 'Small size input',
59
+ },
60
+ };
61
+
62
+ // Medium 사이즈
63
+ export const Medium: Story = {
64
+ args: {
65
+ size: 'medium',
66
+ label: true,
67
+ labelText: 'Medium Input',
68
+ placeholder: 'Medium size input',
69
+ },
70
+ };
71
+
72
+ // Large 사이즈
73
+ export const Large: Story = {
74
+ args: {
75
+ size: 'large',
76
+ label: true,
77
+ labelText: 'Large Input',
78
+ placeholder: 'Large size input',
79
+ },
80
+ };
81
+
82
+ // Label 없음
83
+ export const WithoutLabel: Story = {
84
+ args: {
85
+ size: 'medium',
86
+ label: false,
87
+ placeholder: 'Input without label',
88
+ },
89
+ };
90
+
91
+ // Disabled 상태
92
+ export const Disabled: Story = {
93
+ args: {
94
+ size: 'medium',
95
+ label: true,
96
+ labelText: 'Disabled Input',
97
+ placeholder: 'This input is disabled',
98
+ disabled: true,
99
+ },
100
+ };
101
+
102
+ // 인터랙티브 예제 (Controlled Input)
103
+ export const Controlled: Story = {
104
+ render: (args) => {
105
+ const [value, setValue] = useState('');
106
+
107
+ return (
108
+ <div style={{ width: '300px' }}>
109
+ <Input
110
+ {...args}
111
+ value={value}
112
+ onChange={(e) => setValue(e.target.value)}
113
+ />
114
+ <p style={{ marginTop: '10px', fontSize: '14px', color: '#666' }}>
115
+ Current value: {value || '(empty)'}
116
+ </p>
117
+ </div>
118
+ );
119
+ },
120
+ args: {
121
+ size: 'medium',
122
+ label: true,
123
+ labelText: 'Controlled Input',
124
+ placeholder: 'Type something...',
125
+ },
126
+ };
127
+
128
+ // 모든 사이즈 비교
129
+ export const AllSizes: Story = {
130
+ render: () => (
131
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '20px', width: '300px' }}>
132
+ <Input
133
+ size="small"
134
+ label={true}
135
+ labelText="Small"
136
+ placeholder="Small input"
137
+ />
138
+ <Input
139
+ size="medium"
140
+ label={true}
141
+ labelText="Medium"
142
+ placeholder="Medium input"
143
+ />
144
+ <Input
145
+ size="large"
146
+ label={true}
147
+ labelText="Large"
148
+ placeholder="Large input"
149
+ />
150
+ </div>
151
+ ),
152
+ };
@@ -0,0 +1,33 @@
1
+
2
+ import React,{ useState } from 'react';
3
+ import { getInputWrapperStyle, labelStyle, getInputStyle } from './style';
4
+ import { InputProps } from '../../types/input.types'
5
+
6
+ const Input = ({
7
+ size = 'medium',
8
+ label = false,
9
+ labelText = '',
10
+ placeholder = '',
11
+ value,
12
+ onChange,
13
+ disabled = false,
14
+ }: InputProps) => {
15
+ const [isFocused, setIsFocused] = useState(false);
16
+
17
+ return (
18
+ <div style={getInputWrapperStyle(size)}>
19
+ {label && labelText && <label style={labelStyle}>{labelText}</label>}
20
+ <input
21
+ style={getInputStyle(size, disabled, isFocused)}
22
+ placeholder={placeholder}
23
+ value={value}
24
+ onChange={onChange}
25
+ disabled={disabled}
26
+ onFocus={() => setIsFocused(true)}
27
+ onBlur={() => setIsFocused(false)}
28
+ />
29
+ </div>
30
+ );
31
+ };
32
+
33
+ export default Input;
@@ -0,0 +1 @@
1
+ export * from './Input'
@@ -0,0 +1,88 @@
1
+ import React from 'react';
2
+ import { typography } from '@/style';
3
+
4
+ export const getInputWrapperStyle = (size: 'small' | 'medium' | 'large'): React.CSSProperties => {
5
+ let height = '48px';
6
+
7
+ switch (size) {
8
+ case 'small':
9
+ height = '41px';
10
+ break;
11
+ case 'medium':
12
+ height = '48px';
13
+ break;
14
+ case 'large':
15
+ height = '56px';
16
+ break;
17
+ }
18
+
19
+ return {
20
+ display: 'flex',
21
+ flexDirection: 'column',
22
+ gap: '8px',
23
+ width: '306px',
24
+ height: height,
25
+ };
26
+ };
27
+
28
+ export const labelStyle: React.CSSProperties = {
29
+ ...typography.label.small,
30
+ color: '#333',
31
+ };
32
+
33
+ export const getInputStyle = (
34
+ inputSize: 'small' | 'medium' | 'large',
35
+ disabled: boolean,
36
+ isFocused: boolean
37
+ ): React.CSSProperties => {
38
+ let padding = '0px 16px';
39
+ let fontSize = typography.label.medium.fontSize;
40
+ let lineHeight = typography.label.medium.lineHeight;
41
+ let fontWeight = typography.label.medium.fontWeight;
42
+ let height = '48px';
43
+ let width = '306px';
44
+
45
+ switch (inputSize) {
46
+ case 'small':
47
+ fontSize = typography.label.small.fontSize;
48
+ lineHeight = typography.label.small.lineHeight;
49
+ fontWeight = typography.label.small.fontWeight;
50
+ height = '40px';
51
+ break;
52
+ case 'medium':
53
+ fontSize = typography.label.medium.fontSize;
54
+ lineHeight = typography.label.medium.lineHeight;
55
+ fontWeight = typography.label.medium.fontWeight;
56
+ height = '48px';
57
+ break;
58
+ case 'large':
59
+ fontSize = typography.label.large.fontSize;
60
+ lineHeight = typography.label.large.lineHeight;
61
+ fontWeight = typography.label.large.fontWeight;
62
+ height = '56px';
63
+ break;
64
+ }
65
+
66
+ return {
67
+ width,
68
+ height,
69
+ minHeight: height,
70
+ maxHeight: height,
71
+ padding,
72
+ fontSize,
73
+ lineHeight,
74
+ fontWeight,
75
+ fontFamily: typography.fontFamily.primary,
76
+ border: isFocused ? '1px solid #4a90e2' : '1px solid #ddd',
77
+ borderRadius: '4px',
78
+ outline: 'none',
79
+ transition: 'all 0.2s ease',
80
+ boxShadow: isFocused ? '0 0 0 3px rgba(74, 144, 226, 0.1)' : 'none',
81
+ backgroundColor: disabled ? '#f5f5f5' : '#fff',
82
+ cursor: disabled ? 'not-allowed' : 'text',
83
+ color: '#333',
84
+ boxSizing: 'border-box',
85
+ display: 'flex',
86
+ alignItems: 'center',
87
+ };
88
+ };
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import Sidebar from './Sidebar';
4
+
5
+ const meta: Meta<typeof Sidebar> = {
6
+ title: 'Components/Sidebar',
7
+ component: Sidebar,
8
+ parameters: {
9
+ layout: 'centered',
10
+ },
11
+ tags: ['autodocs'],
12
+ argTypes: {
13
+ buttonStyle: {
14
+ control: 'text'
15
+ },
16
+ description: {
17
+ control: 'object',
18
+ }
19
+ },
20
+ };
21
+
22
+ export default meta;
23
+ type Story = StoryObj<typeof meta>;
24
+
25
+ export const Default: Story = {
26
+ args: {
27
+ buttonStyle: 'one',
28
+ title: ['입력 방법','삭제 방법'],
29
+ description: [['입력을 하기위해 더블클릭 또는 Enter키를 치세요.', '성명 입력 후에는 반드시 Enter키를 치세요.', '정식 과목명에는 과목의 실제 명칭을 기제하세요.(예. 수학 과목이 여러개인 경우 수학1: X 수학: O)','단축 과목명에는 시간표에 들어갈 과목 명칭을 작성하세요'],['중간의 과목은 삭제 할 수 없습니다.', '맨 끝 과목만 삭제 가능합니다.']]
30
+ },
31
+ };
32
+
33
+ export const Default2: Story = {
34
+ args: {
35
+ buttonStyle: 'two',
36
+ title: ['입력 방법','삭제 방법','입력 방법','삭제 방법','입력 방법','삭제 방법','입력 방법','삭제 방법'],
37
+ description: [['입력을 하기위해 더블클릭 또는 Enter키를 치세요.', '성명 입력 후에는 반드시 Enter키를 치세요.', '정식 과목명에는 과목의 실제 명칭을 기제하세요.(예. 수학 과목이 여러개인 경우 수학1: X 수학: O)','단축 과목명에는 시간표에 들어갈 과목 명칭을 작성하세요'],['중간의 과목은 삭제 할 수 없습니다.', '맨 끝 과목만 삭제 가능합니다.'],['입력을 하기위해 더블클릭 또는 Enter키를 치세요.', '성명 입력 후에는 반드시 Enter키를 치세요.', '정식 과목명에는 과목의 실제 명칭을 기제하세요.(예. 수학 과목이 여러개인 경우 수학1: X 수학: O)','단축 과목명에는 시간표에 들어갈 과목 명칭을 작성하세요'],['중간의 과목은 삭제 할 수 없습니다.', '맨 끝 과목만 삭제 가능합니다.'],['입력을 하기위해 더블클릭 또는 Enter키를 치세요.', '성명 입력 후에는 반드시 Enter키를 치세요.', '정식 과목명에는 과목의 실제 명칭을 기제하세요.(예. 수학 과목이 여러개인 경우 수학1: X 수학: O)','단축 과목명에는 시간표에 들어갈 과목 명칭을 작성하세요'],['중간의 과목은 삭제 할 수 없습니다.', '맨 끝 과목만 삭제 가능합니다.'],['입력을 하기위해 더블클릭 또는 Enter키를 치세요.', '성명 입력 후에는 반드시 Enter키를 치세요.', '정식 과목명에는 과목의 실제 명칭을 기제하세요.(예. 수학 과목이 여러개인 경우 수학1: X 수학: O)','단축 과목명에는 시간표에 들어갈 과목 명칭을 작성하세요'],['중간의 과목은 삭제 할 수 없습니다.', '맨 끝 과목만 삭제 가능합니다.']]
38
+ },
39
+ };
40
+
41
+ export const Default3: Story = {
42
+ args: {
43
+ buttonStyle: 'two',
44
+ title: ['입력 방법', '삭제 방법'],
45
+ description: [['입력을 하기위해 ', '성명 입력 ', '정식 칭을 작성하세요'], ['중간의 과목습니다.', '맨 끝 과목합니다.']]
46
+ },
47
+ };
48
+
49
+ export const WithBackground: Story = {
50
+ parameters: {
51
+ backgrounds: {
52
+ default: 'light',
53
+ values: [
54
+ { name: 'light', value: '#f4f5f6' },
55
+ { name: 'dark', value: '#202124' },
56
+ ],
57
+ },
58
+ },
59
+ };
60
+
@@ -0,0 +1,67 @@
1
+ import React from "react";
2
+ import {
3
+ StyledButtonArea,
4
+ StyledDescription,
5
+ StyledDisclosure,
6
+ StyledOpenContents, StyledOpenContentsText,
7
+ StyledSidebar,
8
+ StyledTitle,
9
+ StyledIcon
10
+ } from './style'
11
+ import Button from "../Button/Button";
12
+ import {SidebarProps} from "../../type/Sidebar.types";
13
+ import icon from "../../assets/icons/icon_size.png"
14
+
15
+ export default function Sidebar({buttonStyle, description, title}: SidebarProps) {
16
+
17
+ const [openSections, setOpenSections] = React.useState<boolean[]>(
18
+ Array(description.length).fill(true) // 기본적으로 모두 열려있게
19
+ );
20
+
21
+ const toggleSection = (index: number) => {
22
+ const newState = [...openSections];
23
+ newState[index] = !newState[index]; // 현재 인덱스 toggle
24
+ setOpenSections(newState);
25
+ };
26
+
27
+ return(
28
+ <StyledSidebar>
29
+ <StyledDescription>
30
+ {title.map((item, index) => (
31
+ <StyledDisclosure>
32
+ <StyledTitle onClick={() => toggleSection(index)} style={{ cursor: 'pointer' }}>
33
+ <StyledIcon src={icon} alt="icon" isOpen={openSections[index]} />
34
+ {item}
35
+ </StyledTitle>
36
+
37
+ {openSections[index] && (
38
+ <StyledOpenContents>
39
+ {description[index].map((option, i) => (
40
+ <StyledOpenContentsText key={i}>{option}</StyledOpenContentsText>
41
+ ))}
42
+ </StyledOpenContents>
43
+ )}
44
+ </StyledDisclosure>
45
+ ))}
46
+ </StyledDescription>
47
+ {buttonStyle === 'two' ? <TwoButton /> : <OneButton />}
48
+ </StyledSidebar>
49
+ )
50
+ }
51
+
52
+ function OneButton() {
53
+ return (
54
+ <StyledButtonArea>
55
+ <Button size='small' type='primary' label='다음으로' width='240px' height='40px' ></Button>
56
+ </StyledButtonArea>
57
+ )
58
+ }
59
+
60
+ function TwoButton() {
61
+ return (
62
+ <StyledButtonArea>
63
+ <Button size='small' type='secondary' label='이전으로' width='115px' height='40px' ></Button>
64
+ <Button size='small' type='primary' label='다음으로' width='115px' height='40px' ></Button>
65
+ </StyledButtonArea>
66
+ )
67
+ }
@@ -0,0 +1 @@
1
+ export * from './Sidebar';
@@ -0,0 +1 @@
1
+ export * from './Sidebar'
@@ -0,0 +1,109 @@
1
+ import styled from '@emotion/styled';
2
+ import { colors } from '../../style';
3
+ import { typography } from '../../style';
4
+
5
+ interface TitleProps {
6
+ isOpen: boolean;
7
+
8
+ }
9
+
10
+ export const StyledIcon = styled.img<TitleProps>`
11
+ width: 20px;
12
+ height: 20px;
13
+ transition: transform 0.3s ease;
14
+ transform: rotate(${props => (props.isOpen ? '0deg' : '90deg')});
15
+ `
16
+
17
+ export const StyledSidebar = styled.div`
18
+ display: flex;
19
+ width: 240px;
20
+ height: 100vh;
21
+ padding: 16px;
22
+ flex-direction: column;
23
+ align-items: flex-start;
24
+ gap: 16px;
25
+
26
+ background: ${colors.background.white};
27
+ `;
28
+
29
+ export const StyledDescription = styled.div`
30
+ width: 100%;
31
+ display: flex;
32
+ flex-direction: column;
33
+ align-items: flex-start;
34
+ gap: 18px;
35
+ flex: 1 0 0;
36
+ max-height: calc(100vh - 40px);
37
+ overflow-y: auto;
38
+ overflow-x: hidden;
39
+
40
+ ::-webkit-scrollbar {
41
+ width: 6px;
42
+ }
43
+
44
+ ::-webkit-scrollbar-track {
45
+ background: ${colors.surface["gray-subtler"]};
46
+ border-radius: 4px;
47
+ }
48
+
49
+ ::-webkit-scrollbar-thumb {
50
+ background-color: ${colors.text.subtle};
51
+ border-radius: 4px;
52
+ }
53
+
54
+ ::-webkit-scrollbar-thumb:hover {
55
+ background-color: ${colors.action["secondary-active"]};
56
+ }
57
+
58
+ ::-webkit-scrollbar-thumb:active {
59
+ background-color: ${colors.action["primary-active"]};
60
+ }
61
+ `
62
+
63
+ export const StyledButtonArea = styled.div`
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 10px;
67
+ align-self: stretch;
68
+
69
+ `
70
+
71
+ export const StyledDisclosure = styled.div`
72
+ display: flex;
73
+ gap: 4px;
74
+ flex-direction: column;
75
+
76
+ `
77
+ export const StyledTitle = styled.div`
78
+ display: flex;
79
+ width: 147px;
80
+ align-items: center;
81
+ gap: 4px;
82
+ height: 26px;
83
+ user-select: none;
84
+ `
85
+ export const StyledOpenContents = styled.div`
86
+ display: flex;
87
+ padding: 12px 16px;
88
+ flex-direction: column;
89
+ align-items: flex-start;
90
+ gap: 8px;
91
+ align-self: stretch;
92
+ max-width: 186px;
93
+
94
+ border-radius: 12px;
95
+ background: ${colors.surface["gray-subtler"]};
96
+ `
97
+ export const StyledOpenContentsText = styled.p`
98
+ align-self: stretch;
99
+
100
+ color: ${colors.text.subtle};
101
+
102
+ font-family: ${typography.fontFamily.primary};
103
+ font-size: 13px;
104
+ font-style: normal;
105
+ font-weight: 400;
106
+ line-height: 150%; /* 19.5px */
107
+ letter-spacing: 0;
108
+ margin: 0;
109
+ `
@@ -0,0 +1,116 @@
1
+ import { Meta, StoryObj } from "@storybook/react";
2
+ import TabBar from "./TabBar";
3
+ import React from "react";
4
+
5
+ const meta = {
6
+ title: "Components/TabBar",
7
+ component: TabBar,
8
+ parameters: {
9
+ layout: "centered",
10
+ },
11
+ tags: ["autodocs"],
12
+ argTypes: {
13
+ title: {
14
+ control: "text",
15
+ description: "탭바 제목",
16
+ },
17
+ items: {
18
+ description: "탭 아이템 목록",
19
+ },
20
+ defaultSelectedId: {
21
+ control: "text",
22
+ description: "기본 선택된 탭 ID",
23
+ },
24
+ onChange: {
25
+ action: "changed",
26
+ description: "탭 변경 이벤트 핸들러",
27
+ },
28
+ },
29
+ } satisfies Meta<typeof TabBar>;
30
+
31
+ export default meta;
32
+ type Story = StoryObj<typeof meta>;
33
+
34
+ const sampleItems = [
35
+ { id: "1", label: "기본 정보 등록" },
36
+ { id: "2", label: "교사명 등록" },
37
+ { id: "3", label: "과목명 등록" },
38
+ { id: "4", label: "요일별 시수 등록" },
39
+ { id: "5", label: "시수표 등록" },
40
+ { id: "6", label: "일과시간 등록" },
41
+ ];
42
+
43
+ // 기본 TabBar
44
+ export const Default: Story = {
45
+ args: {
46
+ title: "시간표 설정",
47
+ items: sampleItems,
48
+ defaultSelectedId: "4",
49
+ },
50
+ };
51
+
52
+ // 첫 번째 항목 선택
53
+ export const FirstSelected: Story = {
54
+ args: {
55
+ title: "시간표 설정",
56
+ items: sampleItems,
57
+ defaultSelectedId: "1",
58
+ },
59
+ };
60
+
61
+ // 마지막 항목 선택
62
+ export const LastSelected: Story = {
63
+ args: {
64
+ title: "시간표 설정",
65
+ items: sampleItems,
66
+ defaultSelectedId: "6",
67
+ },
68
+ };
69
+
70
+ // 적은 항목
71
+ export const FewItems: Story = {
72
+ args: {
73
+ title: "간단한 설정",
74
+ items: [
75
+ { id: "1", label: "기본 정보 등록" },
76
+ { id: "2", label: "교사명 등록" },
77
+ { id: "3", label: "과목명 등록" },
78
+ ],
79
+ defaultSelectedId: "2",
80
+ },
81
+ };
82
+
83
+ // 많은 항목
84
+ export const ManyItems: Story = {
85
+ args: {
86
+ title: "전체 설정",
87
+ items: [
88
+ { id: "1", label: "기본 정보 등록" },
89
+ { id: "2", label: "교사명 등록" },
90
+ { id: "3", label: "과목명 등록" },
91
+ { id: "4", label: "요일별 시수 등록" },
92
+ { id: "5", label: "시수표 등록" },
93
+ { id: "6", label: "일과시간 등록" },
94
+ { id: "7", label: "추가 설정" },
95
+ { id: "8", label: "고급 옵션" },
96
+ ],
97
+ defaultSelectedId: "1",
98
+ },
99
+ };
100
+
101
+ // 인터랙티브 예제
102
+ export const Interactive: Story = {
103
+ args: {
104
+ title: "시간표 설정",
105
+ items: sampleItems,
106
+ defaultSelectedId: "4",
107
+ },
108
+ render: (args) => (
109
+ <div style={{ display: "flex", gap: "16px" }}>
110
+ <TabBar {...args} onChange={(id) => console.log("Selected:", id)} />
111
+ <p style={{ margin: 0, fontSize: "14px", color: "#666" }}>
112
+ 탭을 클릭하여 선택 상태를 변경하세요
113
+ </p>
114
+ </div>
115
+ ),
116
+ };
@@ -0,0 +1,47 @@
1
+ import { useState } from "react";
2
+ import React from "react";
3
+ import * as S from "./style";
4
+
5
+ export interface TabItem {
6
+ id: string;
7
+ label: string;
8
+ }
9
+
10
+ export interface TabBarProps {
11
+ title?: string;
12
+ items: TabItem[];
13
+ defaultSelectedId?: string;
14
+ onChange?: (id: string) => void;
15
+ }
16
+
17
+ const TabBar = ({ title, items, defaultSelectedId, onChange }: TabBarProps) => {
18
+ const [selectedId, setSelectedId] = useState(
19
+ defaultSelectedId || items[0]?.id
20
+ );
21
+
22
+ const handleTabClick = (id: string) => {
23
+ setSelectedId(id);
24
+ if (onChange) {
25
+ onChange(id);
26
+ }
27
+ };
28
+
29
+ return (
30
+ <S.Wrapper>
31
+ {title && <S.Title>{title}</S.Title>}
32
+ <S.Tablist>
33
+ {items.map((item) => (
34
+ <S.TabItem
35
+ key={item.id}
36
+ isSelected={selectedId === item.id}
37
+ onClick={() => handleTabClick(item.id)}
38
+ >
39
+ {item.label}
40
+ </S.TabItem>
41
+ ))}
42
+ </S.Tablist>
43
+ </S.Wrapper>
44
+ );
45
+ };
46
+
47
+ export default TabBar;
@@ -0,0 +1 @@
1
+ export * from "./TabBar";