notionsoft-ui 1.0.27 → 1.0.29
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.
- package/package.json +1 -1
- package/src/notion-ui/phone-input/PhoneInput.stories.tsx +112 -0
- package/src/notion-ui/phone-input/phone-input.tsx +84 -28
- package/src/notion-ui/textarea/Textarea.stories.tsx +77 -0
- package/src/notion-ui/textarea/index.ts +3 -0
- package/src/notion-ui/textarea/textarea.tsx +121 -0
package/package.json
CHANGED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import PhoneInput from "./phone-input";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof PhoneInput> = {
|
|
5
|
+
title: "Form/PhoneInput",
|
|
6
|
+
component: PhoneInput,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: "centered",
|
|
9
|
+
},
|
|
10
|
+
args: {
|
|
11
|
+
placeholder: "Phone number",
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default meta;
|
|
16
|
+
|
|
17
|
+
type Story = StoryObj<typeof PhoneInput>;
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------
|
|
20
|
+
// Default
|
|
21
|
+
// ---------------------------------------------------------------------
|
|
22
|
+
export const Default: Story = {
|
|
23
|
+
args: {
|
|
24
|
+
placeholder: "Enter phone number",
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------
|
|
29
|
+
// With Label
|
|
30
|
+
// ---------------------------------------------------------------------
|
|
31
|
+
export const WithLabel: Story = {
|
|
32
|
+
args: {
|
|
33
|
+
label: "Phone Number",
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------
|
|
38
|
+
// Required Hint (*)
|
|
39
|
+
// ---------------------------------------------------------------------
|
|
40
|
+
export const Required: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
label: "Phone Number",
|
|
43
|
+
requiredHint: "*",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------
|
|
48
|
+
// With Error
|
|
49
|
+
// ---------------------------------------------------------------------
|
|
50
|
+
export const WithError: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
label: "Contact Number",
|
|
53
|
+
errorMessage: "Invalid phone number",
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------
|
|
58
|
+
// Pre-filled value
|
|
59
|
+
// ---------------------------------------------------------------------
|
|
60
|
+
export const PreFilled: Story = {
|
|
61
|
+
args: {
|
|
62
|
+
label: "Phone Number",
|
|
63
|
+
value: "+93 700000000",
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------
|
|
68
|
+
// Sizes (sm, md, lg)
|
|
69
|
+
// ---------------------------------------------------------------------
|
|
70
|
+
export const Small: Story = {
|
|
71
|
+
args: {
|
|
72
|
+
label: "Small",
|
|
73
|
+
measurement: "sm",
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const Medium: Story = {
|
|
78
|
+
args: {
|
|
79
|
+
label: "Medium",
|
|
80
|
+
measurement: "md",
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const Large: Story = {
|
|
85
|
+
args: {
|
|
86
|
+
label: "Large",
|
|
87
|
+
measurement: "lg",
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------
|
|
92
|
+
// Read-Only
|
|
93
|
+
// ---------------------------------------------------------------------
|
|
94
|
+
export const ReadOnly: Story = {
|
|
95
|
+
args: {
|
|
96
|
+
readOnly: true,
|
|
97
|
+
value: "+93 700000000",
|
|
98
|
+
label: "Read-only Phone",
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------
|
|
103
|
+
// Custom Root Class Styles
|
|
104
|
+
// ---------------------------------------------------------------------
|
|
105
|
+
export const CustomRootClass: Story = {
|
|
106
|
+
args: {
|
|
107
|
+
label: "Custom Style",
|
|
108
|
+
classNames: {
|
|
109
|
+
rootDivClassName: "p-4 bg-blue-50 rounded-md",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defaultCountries } from "./country-data";
|
|
2
2
|
import { LazyFlag } from "./lazy-flag";
|
|
3
3
|
import type { ParsedCountry } from "./type";
|
|
4
|
-
import { cn } from "
|
|
4
|
+
import { cn } from "../../utils/cn";
|
|
5
5
|
import React, {
|
|
6
6
|
useState,
|
|
7
7
|
useRef,
|
|
@@ -10,6 +10,7 @@ import React, {
|
|
|
10
10
|
useMemo,
|
|
11
11
|
} from "react";
|
|
12
12
|
import { createPortal } from "react-dom";
|
|
13
|
+
import AnimatedItem from "../animated-item";
|
|
13
14
|
|
|
14
15
|
interface VirtualListProps {
|
|
15
16
|
items: ParsedCountry[];
|
|
@@ -100,10 +101,19 @@ const PhoneInput: React.FC<PhoneInputProps> = ({
|
|
|
100
101
|
}) => {
|
|
101
102
|
const { rootDivClassName, iconClassName = "size-4" } = classNames || {};
|
|
102
103
|
const [open, setOpen] = useState(false);
|
|
103
|
-
const [country, setCountry] = useState<ParsedCountry>(defaultCountries[0]);
|
|
104
104
|
const [highlightedIndex, setHighlightedIndex] = useState<number>(0);
|
|
105
|
+
const initialCountry = (() => {
|
|
106
|
+
if (typeof value === "string" && value.startsWith("+")) {
|
|
107
|
+
const matched = defaultCountries.find((c) =>
|
|
108
|
+
value.startsWith("+" + c.dialCode)
|
|
109
|
+
);
|
|
110
|
+
return matched || defaultCountries[0];
|
|
111
|
+
}
|
|
112
|
+
return defaultCountries[0];
|
|
113
|
+
})();
|
|
114
|
+
const [country, setCountry] = useState<ParsedCountry>(initialCountry);
|
|
105
115
|
const [phone, setPhone] = useState<string>(
|
|
106
|
-
typeof value
|
|
116
|
+
typeof value === "string" ? value : `+${initialCountry.dialCode}`
|
|
107
117
|
);
|
|
108
118
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
109
119
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
@@ -118,6 +128,26 @@ const PhoneInput: React.FC<PhoneInputProps> = ({
|
|
|
118
128
|
const chooseCountry = (c: ParsedCountry) => {
|
|
119
129
|
setCountry(c);
|
|
120
130
|
setOpen(false);
|
|
131
|
+
|
|
132
|
+
setPhone((prev) => {
|
|
133
|
+
const oldDialRegex = new RegExp(`^\\+${country.dialCode}`);
|
|
134
|
+
const restNumber = prev.replace(oldDialRegex, "");
|
|
135
|
+
const newValue = `+${c.dialCode}${restNumber}`;
|
|
136
|
+
if (onChange && inputRef.current) {
|
|
137
|
+
const fakeEvent = {
|
|
138
|
+
target: {
|
|
139
|
+
...inputRef.current,
|
|
140
|
+
name: inputRef.current.name,
|
|
141
|
+
value: newValue,
|
|
142
|
+
},
|
|
143
|
+
} as React.ChangeEvent<HTMLInputElement>;
|
|
144
|
+
|
|
145
|
+
onChange(fakeEvent);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return newValue;
|
|
149
|
+
});
|
|
150
|
+
|
|
121
151
|
inputRef.current?.focus();
|
|
122
152
|
};
|
|
123
153
|
|
|
@@ -244,39 +274,40 @@ const PhoneInput: React.FC<PhoneInputProps> = ({
|
|
|
244
274
|
measurement == "lg"
|
|
245
275
|
? {
|
|
246
276
|
height: "50px",
|
|
247
|
-
endContent: label
|
|
248
|
-
? "ltr:top-[48px] rtl:top-[54px]-translate-y-1/2"
|
|
249
|
-
: "top-[26px] -translate-y-1/2",
|
|
250
|
-
startContent: label
|
|
251
|
-
? "ltr:top-[48px] rtl:top-[54px] -translate-y-1/2"
|
|
252
|
-
: "top-[26px] -translate-y-1/2",
|
|
253
277
|
required: label ? "ltr:top-[4px] rtl:top-[12px]" : "top-[-19px]",
|
|
254
278
|
}
|
|
255
279
|
: measurement == "md"
|
|
256
280
|
? {
|
|
257
281
|
height: "44px",
|
|
258
|
-
endContent: label
|
|
259
|
-
? "ltr:top-[45px] rtl:top-[51px] -translate-y-1/2"
|
|
260
|
-
: "top-[22px] -translate-y-1/2",
|
|
261
|
-
startContent: label
|
|
262
|
-
? "ltr:top-[45px] rtl:top-[51px] -translate-y-1/2"
|
|
263
|
-
: "top-[22px] -translate-y-1/2",
|
|
264
282
|
required: label ? "ltr:top-[4px] rtl:top-[12px]" : "top-[-19px]",
|
|
265
283
|
}
|
|
266
284
|
: {
|
|
267
285
|
height: "40px",
|
|
268
|
-
endContent: label
|
|
269
|
-
? "ltr:top-[44px] rtl:top-[50px] -translate-y-1/2"
|
|
270
|
-
: "top-[20px] -translate-y-1/2",
|
|
271
|
-
startContent: label
|
|
272
|
-
? "ltr:top-[44px] rtl:top-[50px] -translate-y-1/2"
|
|
273
|
-
: "top-[20px] -translate-y-1/2",
|
|
274
286
|
required: label ? "ltr:top-[4px] rtl:top-[12px]" : "top-[-19px]",
|
|
275
287
|
},
|
|
276
288
|
[measurement, label]
|
|
277
289
|
);
|
|
278
290
|
const readOnlyStyle = readOnly && "opacity-40";
|
|
279
291
|
|
|
292
|
+
const inputChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
293
|
+
let val = e.target.value;
|
|
294
|
+
let name = e.target.name;
|
|
295
|
+
|
|
296
|
+
// Ensure dial code always at start
|
|
297
|
+
if (!val.startsWith(`+${country.dialCode}`)) {
|
|
298
|
+
val = `+${country.dialCode}${val.replace(/^\+\d*/, "")}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
setPhone(val);
|
|
302
|
+
if (onChange) {
|
|
303
|
+
// emit event
|
|
304
|
+
const fakeEvent = {
|
|
305
|
+
...e,
|
|
306
|
+
target: { ...e.target, name: name, value: val },
|
|
307
|
+
};
|
|
308
|
+
onChange(fakeEvent as React.ChangeEvent<HTMLInputElement>);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
280
311
|
return (
|
|
281
312
|
<div
|
|
282
313
|
className={cn(
|
|
@@ -329,11 +360,12 @@ const PhoneInput: React.FC<PhoneInputProps> = ({
|
|
|
329
360
|
<input
|
|
330
361
|
ref={inputRef}
|
|
331
362
|
type="tel"
|
|
332
|
-
value={
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
363
|
+
value={
|
|
364
|
+
phone.startsWith(`+${country.dialCode}`)
|
|
365
|
+
? phone
|
|
366
|
+
: `+${country.dialCode}`
|
|
367
|
+
}
|
|
368
|
+
onChange={inputChanged}
|
|
337
369
|
placeholder="Phone number"
|
|
338
370
|
style={{
|
|
339
371
|
height: heightStyle.height,
|
|
@@ -378,9 +410,8 @@ const PhoneInput: React.FC<PhoneInputProps> = ({
|
|
|
378
410
|
renderRow={(c, i) => (
|
|
379
411
|
<div
|
|
380
412
|
onClick={() => chooseCountry(c)}
|
|
381
|
-
onMouseEnter={() => setHighlightedIndex(i)}
|
|
382
413
|
className={`flex ltr:text-sm rtl:text-sm rtl:font-semibold items-center gap-2 px-2 py-1 cursor-pointer ${
|
|
383
|
-
i == highlightedIndex
|
|
414
|
+
i == highlightedIndex && "bg-primary/5"
|
|
384
415
|
}`}
|
|
385
416
|
role="option"
|
|
386
417
|
aria-selected={i === highlightedIndex}
|
|
@@ -394,6 +425,31 @@ const PhoneInput: React.FC<PhoneInputProps> = ({
|
|
|
394
425
|
</div>,
|
|
395
426
|
document.body
|
|
396
427
|
)}
|
|
428
|
+
{/* Error Message */}
|
|
429
|
+
{hasError && (
|
|
430
|
+
<AnimatedItem
|
|
431
|
+
springProps={{
|
|
432
|
+
from: {
|
|
433
|
+
opacity: 0,
|
|
434
|
+
transform: "translateY(-8px)",
|
|
435
|
+
},
|
|
436
|
+
config: {
|
|
437
|
+
mass: 1,
|
|
438
|
+
tension: 210,
|
|
439
|
+
friction: 20,
|
|
440
|
+
},
|
|
441
|
+
to: {
|
|
442
|
+
opacity: 1,
|
|
443
|
+
transform: "translateY(0px)",
|
|
444
|
+
},
|
|
445
|
+
}}
|
|
446
|
+
intersectionArgs={{ once: true, rootMargin: "-5% 0%" }}
|
|
447
|
+
>
|
|
448
|
+
<h1 className="text-red-400 text-start capitalize rtl:text-sm rtl:font-medium ltr:text-sm-ltr">
|
|
449
|
+
{errorMessage}
|
|
450
|
+
</h1>
|
|
451
|
+
</AnimatedItem>
|
|
452
|
+
)}
|
|
397
453
|
</div>
|
|
398
454
|
);
|
|
399
455
|
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Textarea } from "./textarea";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Textarea> = {
|
|
5
|
+
title: "Form/Textarea",
|
|
6
|
+
component: Textarea,
|
|
7
|
+
args: {
|
|
8
|
+
placeholder: "Write something...",
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof Textarea>;
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------
|
|
15
|
+
// Default
|
|
16
|
+
// ---------------------------------------
|
|
17
|
+
export const Default: Story = {
|
|
18
|
+
args: {
|
|
19
|
+
placeholder: "Enter text...",
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------
|
|
24
|
+
// With Label
|
|
25
|
+
// ---------------------------------------
|
|
26
|
+
export const WithLabel: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
label: "Description",
|
|
29
|
+
placeholder: "Enter description...",
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------
|
|
34
|
+
// Required Hint (*)
|
|
35
|
+
// ---------------------------------------
|
|
36
|
+
export const WithRequired: Story = {
|
|
37
|
+
args: {
|
|
38
|
+
label: "Bio",
|
|
39
|
+
requiredHint: "*",
|
|
40
|
+
placeholder: "Tell us about yourself...",
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------
|
|
45
|
+
// With Error Message (AnimatedItem visible)
|
|
46
|
+
// ---------------------------------------
|
|
47
|
+
export const WithError: Story = {
|
|
48
|
+
args: {
|
|
49
|
+
label: "Comment",
|
|
50
|
+
errorMessage: "Comment is required",
|
|
51
|
+
placeholder: "Add a comment...",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------
|
|
56
|
+
// ReadOnly Example
|
|
57
|
+
// ---------------------------------------
|
|
58
|
+
export const ReadOnly: Story = {
|
|
59
|
+
args: {
|
|
60
|
+
label: "Readonly Field",
|
|
61
|
+
readOnly: true,
|
|
62
|
+
value: "This textarea cannot be edited",
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------
|
|
67
|
+
// Custom Root Class via classNames
|
|
68
|
+
// ---------------------------------------
|
|
69
|
+
export const CustomRootClass: Story = {
|
|
70
|
+
args: {
|
|
71
|
+
label: "Custom Styled",
|
|
72
|
+
placeholder: "Root custom style applied",
|
|
73
|
+
classNames: {
|
|
74
|
+
rootDivClassName: "bg-blue-50 p-3 rounded-lg",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
// import { cn } from "@/utils/cn";
|
|
3
|
+
import { cn } from "../../utils/cn";
|
|
4
|
+
import AnimatedItem from "../../notion-ui/animated-item";
|
|
5
|
+
// import AnimatedItem from "../animated-item";
|
|
6
|
+
|
|
7
|
+
export interface TextareaProps
|
|
8
|
+
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
9
|
+
requiredHint?: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
errorMessage?: string;
|
|
12
|
+
classNames?: {
|
|
13
|
+
rootDivClassName?: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
18
|
+
(
|
|
19
|
+
{
|
|
20
|
+
className,
|
|
21
|
+
requiredHint,
|
|
22
|
+
classNames,
|
|
23
|
+
errorMessage,
|
|
24
|
+
label,
|
|
25
|
+
readOnly,
|
|
26
|
+
...rest
|
|
27
|
+
},
|
|
28
|
+
ref
|
|
29
|
+
) => {
|
|
30
|
+
const hasError = !!errorMessage;
|
|
31
|
+
const { rootDivClassName } = classNames || {};
|
|
32
|
+
const heightStyle = {
|
|
33
|
+
required: label ? "ltr:top-[4px] rtl:top-[12px]" : "top-[-19px]",
|
|
34
|
+
};
|
|
35
|
+
const readOnlyStyle = readOnly && "opacity-40";
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
className={cn(
|
|
40
|
+
rootDivClassName,
|
|
41
|
+
"flex w-full flex-col justify-end",
|
|
42
|
+
readOnlyStyle
|
|
43
|
+
)}
|
|
44
|
+
>
|
|
45
|
+
<div
|
|
46
|
+
className={cn(
|
|
47
|
+
"relative text-start select-none h-fit rtl:text-lg-rtl ltr:text-lg-ltr"
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
{/* Required Hint */}
|
|
51
|
+
{requiredHint && (
|
|
52
|
+
<span
|
|
53
|
+
className={cn(
|
|
54
|
+
"absolute font-semibold text-red-600 rtl:text-[13px] ltr:text-[11px] ltr:right-2.5 rtl:left-2.5",
|
|
55
|
+
heightStyle.required
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
{requiredHint}
|
|
59
|
+
</span>
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
{/* Label */}
|
|
63
|
+
{label && (
|
|
64
|
+
<label
|
|
65
|
+
htmlFor={label}
|
|
66
|
+
className={cn(
|
|
67
|
+
"font-semibold rtl:text-xl-rtl ltr:text-lg-ltr inline-block pb-1"
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
70
|
+
{label}
|
|
71
|
+
</label>
|
|
72
|
+
)}
|
|
73
|
+
|
|
74
|
+
{/* Textarea Field */}
|
|
75
|
+
|
|
76
|
+
<textarea
|
|
77
|
+
ref={ref}
|
|
78
|
+
data-slot="textarea"
|
|
79
|
+
readOnly={readOnly}
|
|
80
|
+
className={cn(
|
|
81
|
+
"border-input placeholder:text-muted-foreground focus-visible:border-ring-0 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-70 md:text-sm focus-visible:border-tertiary/60 focus-visible:shadow-sm",
|
|
82
|
+
"placeholder:text-primary/60 ltr:text-sm rtl:text-sm rtl:font-semibold",
|
|
83
|
+
hasError && "border-red-400",
|
|
84
|
+
className
|
|
85
|
+
)}
|
|
86
|
+
{...rest}
|
|
87
|
+
disabled={readOnly}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
{/* Error Message */}
|
|
92
|
+
{hasError && (
|
|
93
|
+
<AnimatedItem
|
|
94
|
+
springProps={{
|
|
95
|
+
from: {
|
|
96
|
+
opacity: 0,
|
|
97
|
+
transform: "translateY(-8px)",
|
|
98
|
+
},
|
|
99
|
+
config: {
|
|
100
|
+
mass: 1,
|
|
101
|
+
tension: 210,
|
|
102
|
+
friction: 20,
|
|
103
|
+
},
|
|
104
|
+
to: {
|
|
105
|
+
opacity: 1,
|
|
106
|
+
transform: "translateY(0px)",
|
|
107
|
+
},
|
|
108
|
+
}}
|
|
109
|
+
intersectionArgs={{ once: true, rootMargin: "-5% 0%" }}
|
|
110
|
+
>
|
|
111
|
+
<h1 className="text-red-400 text-start capitalize rtl:text-sm rtl:font-medium ltr:text-sm-ltr">
|
|
112
|
+
{errorMessage}
|
|
113
|
+
</h1>
|
|
114
|
+
</AnimatedItem>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
export default Textarea;
|