teachable-design-system 0.1.16 → 0.2.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.
@@ -1,38 +1,50 @@
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';
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
5
 
6
6
  const meta: Meta<InputProps> = {
7
- title: 'Components/Input',
8
- component: Input,
9
- parameters: {
10
- layout: 'centered',
7
+ title: "Components/Input",
8
+ component: Input,
9
+ parameters: {
10
+ layout: "centered",
11
+ },
12
+ tags: ["autodocs"],
13
+ argTypes: {
14
+ width: {
15
+ control: "text",
16
+ description: "인풋의 너비 (예: 100%, 300px, 500px)",
11
17
  },
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
- },
18
+ height: {
19
+ control: "text",
20
+ description: "인풋의 높이 (예: 40px, 50px, 60px)",
35
21
  },
22
+ size: {
23
+ control: "select",
24
+ options: ["small", "medium", "large"],
25
+ description: "인풋의 크기",
26
+ },
27
+ label: {
28
+ control: "boolean",
29
+ description: "label 표시 여부",
30
+ },
31
+ labelText: {
32
+ control: "text",
33
+ description: "label에 표시될 텍스트",
34
+ },
35
+ placeholder: {
36
+ control: "text",
37
+ description: "placeholder 텍스트",
38
+ },
39
+ disabled: {
40
+ control: "boolean",
41
+ description: "비활성화 여부",
42
+ },
43
+ isPassword: {
44
+ control: "boolean",
45
+ description: "비밀번호 입력 여부",
46
+ },
47
+ },
36
48
  } satisfies Meta<typeof Input>;
37
49
 
38
50
  export default meta;
@@ -40,113 +52,230 @@ type Story = StoryObj<typeof meta>;
40
52
 
41
53
  // 기본 스토리
42
54
  export const Default: Story = {
43
- args: {
44
- size: 'medium',
45
- label: true,
46
- labelText: 'Label',
47
- placeholder: 'Enter text...',
48
- disabled: false,
49
- },
55
+ args: {
56
+ size: "medium",
57
+ label: true,
58
+ labelText: "Label",
59
+ placeholder: "Enter text...",
60
+ disabled: false,
61
+ },
50
62
  };
51
63
 
52
64
  // Small 사이즈
53
65
  export const Small: Story = {
54
- args: {
55
- size: 'small',
56
- label: true,
57
- labelText: 'Small Input',
58
- placeholder: 'Small size input',
59
- },
66
+ args: {
67
+ size: "small",
68
+ label: true,
69
+ labelText: "Small Input",
70
+ placeholder: "Small size input",
71
+ },
60
72
  };
61
73
 
62
74
  // Medium 사이즈
63
75
  export const Medium: Story = {
64
- args: {
65
- size: 'medium',
66
- label: true,
67
- labelText: 'Medium Input',
68
- placeholder: 'Medium size input',
69
- },
76
+ args: {
77
+ size: "medium",
78
+ label: true,
79
+ labelText: "Medium Input",
80
+ placeholder: "Medium size input",
81
+ },
70
82
  };
71
83
 
72
84
  // Large 사이즈
73
85
  export const Large: Story = {
74
- args: {
75
- size: 'large',
76
- label: true,
77
- labelText: 'Large Input',
78
- placeholder: 'Large size input',
79
- },
86
+ args: {
87
+ size: "large",
88
+ label: true,
89
+ labelText: "Large Input",
90
+ placeholder: "Large size input",
91
+ },
80
92
  };
81
93
 
82
94
  // Label 없음
83
95
  export const WithoutLabel: Story = {
84
- args: {
85
- size: 'medium',
86
- label: false,
87
- placeholder: 'Input without label',
88
- },
96
+ args: {
97
+ size: "medium",
98
+ label: false,
99
+ placeholder: "Input without label",
100
+ },
89
101
  };
90
102
 
91
103
  // Disabled 상태
92
104
  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
- },
105
+ args: {
106
+ size: "medium",
107
+ label: true,
108
+ labelText: "Disabled Input",
109
+ placeholder: "This input is disabled",
110
+ disabled: true,
111
+ },
100
112
  };
101
113
 
102
114
  // 인터랙티브 예제 (Controlled Input)
103
115
  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
- },
116
+ render: (args) => {
117
+ const [value, setValue] = useState("");
118
+
119
+ return (
120
+ <div style={{ width: "300px" }}>
121
+ <Input
122
+ {...args}
123
+ value={value}
124
+ onChange={(e) => setValue(e.target.value)}
125
+ />
126
+ <p style={{ marginTop: "10px", fontSize: "14px", color: "#666" }}>
127
+ Current value: {value || "(empty)"}
128
+ </p>
129
+ </div>
130
+ );
131
+ },
132
+ args: {
133
+ size: "medium",
134
+ label: true,
135
+ labelText: "Controlled Input",
136
+ placeholder: "Type something...",
137
+ },
126
138
  };
127
139
 
128
140
  // 모든 사이즈 비교
129
141
  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
- };
142
+ render: () => (
143
+ <div
144
+ style={{
145
+ display: "flex",
146
+ flexDirection: "column",
147
+ gap: "20px",
148
+ width: "300px",
149
+ }}
150
+ >
151
+ <Input
152
+ size="small"
153
+ label={true}
154
+ labelText="Small"
155
+ placeholder="Small input"
156
+ />
157
+ <Input
158
+ size="medium"
159
+ label={true}
160
+ labelText="Medium"
161
+ placeholder="Medium input"
162
+ />
163
+ <Input
164
+ size="large"
165
+ label={true}
166
+ labelText="Large"
167
+ placeholder="Large input"
168
+ />
169
+ </div>
170
+ ),
171
+ };
172
+
173
+ // 비밀번호 입력
174
+ export const Password: Story = {
175
+ args: {
176
+ size: "medium",
177
+ label: true,
178
+ labelText: "Password",
179
+ placeholder: "Enter your password",
180
+ isPassword: true,
181
+ },
182
+ };
183
+
184
+ // 비밀번호 입력 (Small)
185
+ export const PasswordSmall: Story = {
186
+ args: {
187
+ size: "small",
188
+ label: true,
189
+ labelText: "Password",
190
+ placeholder: "Enter password",
191
+ isPassword: true,
192
+ },
193
+ };
194
+
195
+ // 비밀번호 입력 (Large)
196
+ export const PasswordLarge: Story = {
197
+ args: {
198
+ size: "large",
199
+ label: true,
200
+ labelText: "Password",
201
+ placeholder: "Enter your password",
202
+ isPassword: true,
203
+ },
204
+ };
205
+
206
+ // 비밀번호 입력 인터랙티브
207
+ export const PasswordControlled: Story = {
208
+ render: (args) => {
209
+ const [value, setValue] = useState("");
210
+
211
+ return (
212
+ <div style={{ width: "300px" }}>
213
+ <Input
214
+ {...args}
215
+ value={value}
216
+ onChange={(e) => setValue(e.target.value)}
217
+ isPassword={true}
218
+ />
219
+ <p style={{ marginTop: "10px", fontSize: "14px", color: "#666" }}>
220
+ Password length: {value.length}
221
+ </p>
222
+ </div>
223
+ );
224
+ },
225
+ args: {
226
+ size: "medium",
227
+ label: true,
228
+ labelText: "Create Password",
229
+ placeholder: "Enter a strong password",
230
+ },
231
+ };
232
+
233
+ // 커스텀 너비
234
+ export const CustomWidth: Story = {
235
+ args: {
236
+ width: "500px",
237
+ size: "medium",
238
+ label: true,
239
+ labelText: "Custom Width Input",
240
+ placeholder: "Width: 500px",
241
+ },
242
+ };
243
+
244
+ // 커스텀 높이
245
+ export const CustomHeight: Story = {
246
+ args: {
247
+ height: "60px",
248
+ size: "medium",
249
+ label: true,
250
+ labelText: "Custom Height Input",
251
+ placeholder: "Height: 60px",
252
+ },
253
+ };
254
+
255
+ // 커스텀 너비와 높이
256
+ export const CustomWidthAndHeight: Story = {
257
+ args: {
258
+ width: "400px",
259
+ height: "70px",
260
+ size: "medium",
261
+ label: true,
262
+ labelText: "Custom Width & Height",
263
+ placeholder: "Width: 400px, Height: 70px",
264
+ },
265
+ };
266
+
267
+ // 퍼센트 너비
268
+ export const PercentageWidth: Story = {
269
+ render: (args) => (
270
+ <div style={{ width: "600px", padding: "20px", border: "1px dashed #ccc" }}>
271
+ <Input {...args} />
272
+ </div>
273
+ ),
274
+ args: {
275
+ width: "100%",
276
+ size: "medium",
277
+ label: true,
278
+ labelText: "100% Width Input",
279
+ placeholder: "This input fills the container",
280
+ },
281
+ };
@@ -1,31 +1,58 @@
1
-
2
- import React,{ useState } from 'react';
3
- import { getInputWrapperStyle, labelStyle, getInputStyle } from './style';
4
- import { InputProps } from '../../types/input.types'
1
+ import React, { useState } from "react";
2
+ import { Eye, EyeOff } from "lucide-react";
3
+ import {
4
+ InputWrapper,
5
+ Label,
6
+ InputContainer,
7
+ StyledInput,
8
+ IconButton,
9
+ } from "./style";
10
+ import { InputProps } from "../../types/input.types";
5
11
 
6
12
  export const Input = ({
7
- size = 'medium',
8
- label = false,
9
- labelText = '',
10
- placeholder = '',
11
- value,
12
- onChange,
13
- disabled = false,
13
+ id,
14
+ width,
15
+ height,
16
+ size = "medium",
17
+ label = false,
18
+ labelText = "",
19
+ placeholder = "",
20
+ value,
21
+ onChange,
22
+ disabled = false,
23
+ isPassword = false,
14
24
  }: InputProps) => {
15
- const [isFocused, setIsFocused] = useState(false);
25
+ const [showPassword, setShowPassword] = useState(false);
26
+
27
+ const inputType = isPassword ? (showPassword ? "text" : "password") : "text";
16
28
 
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
- };
29
+ return (
30
+ <InputWrapper width={width}>
31
+ {label && labelText && <Label htmlFor={id}>{labelText}</Label>}
32
+ <InputContainer>
33
+ <StyledInput
34
+ id={id}
35
+ type={inputType}
36
+ inputSize={size}
37
+ width={width}
38
+ height={height}
39
+ disabled={disabled}
40
+ isPassword={isPassword}
41
+ placeholder={placeholder}
42
+ value={value}
43
+ onChange={onChange}
44
+ />
45
+ {isPassword && (
46
+ <IconButton
47
+ type="button"
48
+ disabled={disabled}
49
+ onClick={() => setShowPassword(!showPassword)}
50
+ aria-label={showPassword ? "Hide password" : "Show password"}
51
+ >
52
+ {showPassword ? <Eye size={20} /> : <EyeOff size={20} />}
53
+ </IconButton>
54
+ )}
55
+ </InputContainer>
56
+ </InputWrapper>
57
+ );
58
+ };
@@ -1 +1 @@
1
- export * from './Input'
1
+ export * from './Input';