gbs-add-block 1.2.5 → 1.2.7
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/README.md +3 -4
- package/index.cjs +1 -0
- package/package.json +36 -30
- package/source/components/button/index.tsx +41 -24
- package/source/components/card/index.tsx +30 -9
- package/source/components/checkbox/index.tsx +24 -15
- package/source/components/datagrid/context/GridContext.tsx +38 -17
- package/source/components/datagrid/createTemplate.tsx +2 -2
- package/source/components/datagrid/hooks/usePagination.ts +0 -8
- package/source/components/datagrid/hooks/useSearch.ts +3 -3
- package/source/components/datagrid/index.tsx +2 -1
- package/source/components/datagrid/layout/GridBody.tsx +7 -4
- package/source/components/datagrid/layout/GridPagination.tsx +3 -2
- package/source/components/datagrid/layout/GridToolbar.tsx +2 -1
- package/source/components/datagrid/type.ts +18 -2
- package/source/components/datepicker/index.tsx +7 -6
- package/source/components/input/index.tsx +141 -143
- package/source/components/modal/index.tsx +151 -139
- package/source/components/modal/types.ts +18 -16
- package/source/components/multiselect/PortalDropDown.tsx +1 -0
- package/source/components/multiselect/index.tsx +5 -4
- package/source/components/navbar/AuthSection.tsx +5 -5
- package/source/components/navbar/NavItemComponent.tsx +2 -2
- package/source/components/select/PortalDropDown.tsx +20 -4
- package/source/components/select/index.tsx +6 -4
- package/source/components/select/types.ts +2 -0
- package/source/components/spinner/index.tsx +2 -2
- package/source/components/tabs/index.tsx +70 -51
- package/source/components/tabs/types.ts +15 -14
- package/source/components/textarea/index.tsx +122 -149
- package/source/components/toast/ToastComponent.tsx +26 -25
- package/source/components/toast/ToastIcon.tsx +8 -8
- package/source/components/uploader/ProgressAnimation.tsx +2 -2
- package/source/components/uploader/UploadedFilePreview.tsx +1 -1
- package/source/components/uploader/index.tsx +1 -1
- package/source/components/uploader/uploaderIcon.tsx +1 -1
- package/source/globalStyle.ts +74 -35
- package/source/icon/iconPaths.ts +6 -0
- package/source/theme.ts +40 -0
|
@@ -153,7 +153,7 @@ const MultiSelect = forwardRef<MultiSelectHandle, MultiSelectProps>(
|
|
|
153
153
|
<Icon
|
|
154
154
|
elements={check}
|
|
155
155
|
svgClass={`h-4 w-4 fill-none ${
|
|
156
|
-
selected.includes(value) ? "stroke-
|
|
156
|
+
selected.includes(value) ? "stroke-black dark:stroke-white" : ""
|
|
157
157
|
}`}
|
|
158
158
|
/>
|
|
159
159
|
{label.length > 20 ? `${label.substring(0, 20)}...` : label}
|
|
@@ -179,7 +179,7 @@ const MultiSelect = forwardRef<MultiSelectHandle, MultiSelectProps>(
|
|
|
179
179
|
return (
|
|
180
180
|
<span
|
|
181
181
|
key={index}
|
|
182
|
-
className="bg-
|
|
182
|
+
className="bg-black text-white dark:bg-zinc-900 px-2 py-1 rounded flex items-center gap-1 text-xs whitespace-nowrap border border-transparent dark:border-zinc-800"
|
|
183
183
|
>
|
|
184
184
|
{label.length > 15 ? `${label.substring(0, 15)}...` : label}
|
|
185
185
|
<span
|
|
@@ -187,7 +187,7 @@ const MultiSelect = forwardRef<MultiSelectHandle, MultiSelectProps>(
|
|
|
187
187
|
e.stopPropagation();
|
|
188
188
|
handleSelect(value);
|
|
189
189
|
}}
|
|
190
|
-
className="cursor-pointer hover:bg-
|
|
190
|
+
className="cursor-pointer hover:bg-zinc-800 dark:hover:bg-zinc-700 rounded p-0.5"
|
|
191
191
|
>
|
|
192
192
|
<Icon elements={x} svgClass="h-3 w-3 stroke-white" />
|
|
193
193
|
</span>
|
|
@@ -195,7 +195,7 @@ const MultiSelect = forwardRef<MultiSelectHandle, MultiSelectProps>(
|
|
|
195
195
|
);
|
|
196
196
|
})}
|
|
197
197
|
{truncate && selected.length > 3 && (
|
|
198
|
-
<span className="bg-
|
|
198
|
+
<span className="bg-black text-white dark:bg-zinc-900 px-2 py-1 rounded text-xs whitespace-nowrap border border-transparent dark:border-zinc-800">
|
|
199
199
|
+{selected.length - 3} more
|
|
200
200
|
</span>
|
|
201
201
|
)}
|
|
@@ -266,3 +266,4 @@ const MultiSelect = forwardRef<MultiSelectHandle, MultiSelectProps>(
|
|
|
266
266
|
|
|
267
267
|
const MemoizedMultiSelect = memo(MultiSelect);
|
|
268
268
|
export { MemoizedMultiSelect as MultiSelect };
|
|
269
|
+
|
|
@@ -27,7 +27,7 @@ const AuthSection: React.FC<AuthSectionProps> = ({
|
|
|
27
27
|
alt={userProfile.name}
|
|
28
28
|
/>
|
|
29
29
|
) : (
|
|
30
|
-
<div className="h-8 w-8 rounded-full bg-
|
|
30
|
+
<div className="h-8 w-8 rounded-full bg-black dark:bg-white text-white dark:text-black flex items-center justify-center font-bold">
|
|
31
31
|
{userProfile.name.charAt(0).toUpperCase()}
|
|
32
32
|
</div>
|
|
33
33
|
)}
|
|
@@ -75,11 +75,11 @@ const AuthSection: React.FC<AuthSectionProps> = ({
|
|
|
75
75
|
<button
|
|
76
76
|
onClick={onLogin}
|
|
77
77
|
className={`
|
|
78
|
-
px-4 py-2 text-sm font-medium rounded-md
|
|
78
|
+
px-4 py-2 text-sm font-medium rounded-md border border-zinc-300 dark:border-zinc-700
|
|
79
79
|
${
|
|
80
80
|
variant === "dark"
|
|
81
|
-
? "text-white hover:bg-
|
|
82
|
-
: "text-
|
|
81
|
+
? "text-white hover:bg-zinc-850"
|
|
82
|
+
: "text-black hover:bg-zinc-100 dark:text-white dark:hover:bg-zinc-900"
|
|
83
83
|
}
|
|
84
84
|
`}
|
|
85
85
|
>
|
|
@@ -87,7 +87,7 @@ const AuthSection: React.FC<AuthSectionProps> = ({
|
|
|
87
87
|
</button>
|
|
88
88
|
<button
|
|
89
89
|
onClick={onSignup}
|
|
90
|
-
className="px-4 py-2 text-sm font-medium text-white bg-
|
|
90
|
+
className="px-4 py-2 text-sm font-medium text-white bg-black dark:bg-white dark:text-black rounded-md hover:bg-zinc-900 dark:hover:bg-zinc-100 transition-colors"
|
|
91
91
|
>
|
|
92
92
|
Sign up
|
|
93
93
|
</button>
|
|
@@ -38,7 +38,7 @@ const NavItemComponent: React.FC<NavItemComponentProps> = ({
|
|
|
38
38
|
onClick={(e) => toggleSubmenu(item.id, e)}
|
|
39
39
|
className={`
|
|
40
40
|
flex items-center justify-between px-3 py-2 rounded-md
|
|
41
|
-
${item.highlight ? "font-
|
|
41
|
+
${item.highlight ? "font-bold text-black dark:text-white" : ""}
|
|
42
42
|
${
|
|
43
43
|
isMobile
|
|
44
44
|
? "w-full text-left"
|
|
@@ -92,7 +92,7 @@ const NavItemComponent: React.FC<NavItemComponentProps> = ({
|
|
|
92
92
|
href={item.href}
|
|
93
93
|
className={`
|
|
94
94
|
flex items-center px-3 py-2 rounded-md
|
|
95
|
-
${item.highlight ? "font-
|
|
95
|
+
${item.highlight ? "font-bold text-black dark:text-white" : ""}
|
|
96
96
|
${
|
|
97
97
|
isMobile
|
|
98
98
|
? "block w-full"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
1
|
+
import { useEffect, useState, useCallback } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
import { popUp } from "../globalStyle";
|
|
4
4
|
|
|
5
5
|
export const PortalDropdown = ({ targetRef, children, isVisible }: any) => {
|
|
6
6
|
const [position, setPosition] = useState({ top: 0, left: 0, width: 0 });
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
if (
|
|
8
|
+
const updatePosition = useCallback(() => {
|
|
9
|
+
if (targetRef && targetRef.current) {
|
|
10
10
|
const rect = targetRef.current.getBoundingClientRect();
|
|
11
11
|
const scrollTop =
|
|
12
12
|
window.pageYOffset || document.documentElement.scrollTop;
|
|
@@ -19,7 +19,23 @@ export const PortalDropdown = ({ targetRef, children, isVisible }: any) => {
|
|
|
19
19
|
width: rect.width,
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
|
-
}, [
|
|
22
|
+
}, [targetRef]);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (isVisible) {
|
|
26
|
+
updatePosition();
|
|
27
|
+
|
|
28
|
+
// Listen to scroll events anywhere in the document (use capture to catch scroll inside elements)
|
|
29
|
+
window.addEventListener("scroll", updatePosition, true);
|
|
30
|
+
// Listen to window resizing
|
|
31
|
+
window.addEventListener("resize", updatePosition);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return () => {
|
|
35
|
+
window.removeEventListener("scroll", updatePosition, true);
|
|
36
|
+
window.removeEventListener("resize", updatePosition);
|
|
37
|
+
};
|
|
38
|
+
}, [isVisible, updatePosition]);
|
|
23
39
|
|
|
24
40
|
if (!isVisible) return null;
|
|
25
41
|
|
|
@@ -40,6 +40,7 @@ const Select = forwardRef<SelectHandle, SelectProps>((props, ref) => {
|
|
|
40
40
|
error = undefined,
|
|
41
41
|
onFiltering,
|
|
42
42
|
disabled = false,
|
|
43
|
+
className = "",
|
|
43
44
|
} = props;
|
|
44
45
|
|
|
45
46
|
applyScrollbarStyles();
|
|
@@ -141,7 +142,7 @@ const Select = forwardRef<SelectHandle, SelectProps>((props, ref) => {
|
|
|
141
142
|
name="search"
|
|
142
143
|
id="search"
|
|
143
144
|
placeholder="Search a value"
|
|
144
|
-
className="w-full outline-none "
|
|
145
|
+
className="w-full bg-transparent outline-none text-slate-900 placeholder:text-slate-400 dark:text-slate-100"
|
|
145
146
|
ref={inputRef}
|
|
146
147
|
value={searchTerm}
|
|
147
148
|
onChange={(e) => {
|
|
@@ -163,7 +164,7 @@ const Select = forwardRef<SelectHandle, SelectProps>((props, ref) => {
|
|
|
163
164
|
}
|
|
164
165
|
}}
|
|
165
166
|
className={`${selectStyle["filter-button"]} ${
|
|
166
|
-
focusedIndex === index ? "bg-
|
|
167
|
+
focusedIndex === index ? "bg-zinc-100 text-black dark:bg-zinc-900 dark:text-white" : ""
|
|
167
168
|
}`}
|
|
168
169
|
onClick={() => handleSelect({ value, label })}
|
|
169
170
|
onMouseEnter={() => setFocusedIndex(index)}
|
|
@@ -171,7 +172,7 @@ const Select = forwardRef<SelectHandle, SelectProps>((props, ref) => {
|
|
|
171
172
|
<Icon
|
|
172
173
|
elements={check}
|
|
173
174
|
svgClass={`h-4 w-4 fill-none ${
|
|
174
|
-
selectedItem === value ? "stroke-
|
|
175
|
+
selectedItem === value ? "stroke-black dark:stroke-white" : ""
|
|
175
176
|
}`}
|
|
176
177
|
/>
|
|
177
178
|
{label.length > 20 ? `${label.substring(0, 20)}...` : label}
|
|
@@ -186,7 +187,7 @@ const Select = forwardRef<SelectHandle, SelectProps>((props, ref) => {
|
|
|
186
187
|
|
|
187
188
|
return (
|
|
188
189
|
<div
|
|
189
|
-
className=
|
|
190
|
+
className={`relative w-full ${className}`}
|
|
190
191
|
ref={selectRef}
|
|
191
192
|
id={id}
|
|
192
193
|
onKeyDown={handleKeyDown}
|
|
@@ -245,3 +246,4 @@ Select.displayName = "Select";
|
|
|
245
246
|
|
|
246
247
|
const MemoizedSelect = memo(Select);
|
|
247
248
|
export { MemoizedSelect as Select };
|
|
249
|
+
|
|
@@ -15,6 +15,7 @@ export type SelectProps = {
|
|
|
15
15
|
selectedItem?: string;
|
|
16
16
|
onFiltering?: (searchTerm: string) => void;
|
|
17
17
|
disabled?: boolean;
|
|
18
|
+
className?: string;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
export type SelectHandle = {
|
|
@@ -25,3 +26,4 @@ export type SelectHandle = {
|
|
|
25
26
|
selectedDisplay: string;
|
|
26
27
|
selected: string | undefined;
|
|
27
28
|
};
|
|
29
|
+
|
|
@@ -11,8 +11,8 @@ import type { SpinnerProps } from "./types";
|
|
|
11
11
|
|
|
12
12
|
export const Spinner: React.FC<SpinnerProps> = ({
|
|
13
13
|
size = "h-10 w-10",
|
|
14
|
-
color = "border-blue-
|
|
15
|
-
fullCircleColor = "border-t-
|
|
14
|
+
color = "border-blue-500",
|
|
15
|
+
fullCircleColor = "border-t-blue-400",
|
|
16
16
|
dotColor = "bg-blue-500",
|
|
17
17
|
duration = "duration-500",
|
|
18
18
|
type = "circle",
|
|
@@ -1,52 +1,71 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) Grampro Business Services and affiliates.
|
|
3
|
-
*
|
|
4
|
-
* This source code is licensed under the MIT license found in the
|
|
5
|
-
* LICENSE file in the root directory of this source tree.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { useState } from "react";
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Grampro Business Services and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
import { twMerge } from "tailwind-merge";
|
|
10
|
+
import type { TabsProps } from "./types";
|
|
11
|
+
|
|
12
|
+
export function Tabs({
|
|
13
|
+
tabs,
|
|
14
|
+
defaultTabId,
|
|
15
|
+
renderContent,
|
|
16
|
+
onChange,
|
|
17
|
+
className = "",
|
|
18
|
+
variant = "line",
|
|
19
|
+
}: TabsProps) {
|
|
20
|
+
const [activeTabId, setActiveTabId] = useState<string>(
|
|
21
|
+
defaultTabId || (tabs.length > 0 ? tabs[0].id : "")
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const handleTabChange = (tabId: string) => {
|
|
25
|
+
setActiveTabId(tabId);
|
|
26
|
+
if (onChange) {
|
|
27
|
+
onChange(tabId);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const isPills = variant === "pills";
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className={twMerge("w-full", className)}>
|
|
35
|
+
<nav
|
|
36
|
+
className={twMerge(
|
|
37
|
+
"flex gap-1",
|
|
38
|
+
isPills
|
|
39
|
+
? "rounded-lg bg-zinc-100 p-1 dark:bg-zinc-900"
|
|
40
|
+
: "border-b border-zinc-200 dark:border-zinc-800"
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
{tabs.map((tab) => {
|
|
44
|
+
const isActive = activeTabId === tab.id;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<button
|
|
48
|
+
key={tab.id}
|
|
49
|
+
type="button"
|
|
50
|
+
disabled={tab.disabled}
|
|
51
|
+
className={twMerge(
|
|
52
|
+
"min-h-9 px-4 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-black dark:focus-visible:ring-white disabled:cursor-not-allowed disabled:text-zinc-400",
|
|
53
|
+
isPills ? "rounded-md" : "-mb-px border-b-2 border-transparent",
|
|
54
|
+
isActive && isPills
|
|
55
|
+
? "bg-white text-zinc-950 shadow-sm dark:bg-zinc-800 dark:text-white"
|
|
56
|
+
: "",
|
|
57
|
+
isActive && !isPills
|
|
58
|
+
? "border-black text-black dark:border-white dark:text-white"
|
|
59
|
+
: "text-zinc-500 hover:text-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-100"
|
|
60
|
+
)}
|
|
61
|
+
onClick={() => !tab.disabled && handleTabChange(tab.id)}
|
|
62
|
+
>
|
|
63
|
+
{tab.label}
|
|
64
|
+
</button>
|
|
65
|
+
);
|
|
66
|
+
})}
|
|
67
|
+
</nav>
|
|
68
|
+
<div className="pt-4">{renderContent(activeTabId)}</div>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
52
71
|
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
export interface TabItem {
|
|
4
|
-
id: string;
|
|
5
|
-
label: string;
|
|
6
|
-
disabled?: boolean;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface TabsProps {
|
|
10
|
-
tabs: TabItem[];
|
|
11
|
-
defaultTabId?: string;
|
|
12
|
-
renderContent: (activeTabId: string) => ReactNode;
|
|
13
|
-
onChange?: (tabId: string) => void;
|
|
14
|
-
className?: string;
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface TabItem {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TabsProps {
|
|
10
|
+
tabs: TabItem[];
|
|
11
|
+
defaultTabId?: string;
|
|
12
|
+
renderContent: (activeTabId: string) => ReactNode;
|
|
13
|
+
onChange?: (tabId: string) => void;
|
|
14
|
+
className?: string;
|
|
15
|
+
variant?: "line" | "pills";
|
|
15
16
|
}
|
|
@@ -1,150 +1,123 @@
|
|
|
1
|
-
import React, { useState, useRef, useEffect } from "react";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const [
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
setCharCount(
|
|
32
|
-
if (onChange) {
|
|
33
|
-
onChange(e);
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
</svg>
|
|
124
|
-
{error}
|
|
125
|
-
</p>
|
|
126
|
-
)}
|
|
127
|
-
{!error && helperText && (
|
|
128
|
-
<p className="text-sm text-gray-500">{helperText}</p>
|
|
129
|
-
)}
|
|
130
|
-
</div>
|
|
131
|
-
|
|
132
|
-
{maxLength && (
|
|
133
|
-
<div className="ml-4 flex-shrink-0">
|
|
134
|
-
<span
|
|
135
|
-
className={`text-sm ${
|
|
136
|
-
charCount > maxLength * 0.9
|
|
137
|
-
? "text-amber-600"
|
|
138
|
-
: charCount === maxLength
|
|
139
|
-
? "text-red-600"
|
|
140
|
-
: "text-gray-400"
|
|
141
|
-
}`}
|
|
142
|
-
>
|
|
143
|
-
{charCount}/{maxLength}
|
|
144
|
-
</span>
|
|
145
|
-
</div>
|
|
146
|
-
)}
|
|
147
|
-
</div>
|
|
148
|
-
</div>
|
|
149
|
-
);
|
|
1
|
+
import React, { useState, useRef, useEffect } from "react";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
import type { TextAreaProps } from "./types";
|
|
4
|
+
|
|
5
|
+
export const TextArea = ({
|
|
6
|
+
label,
|
|
7
|
+
placeholder = "Enter your text here...",
|
|
8
|
+
value,
|
|
9
|
+
onChange,
|
|
10
|
+
error,
|
|
11
|
+
helperText,
|
|
12
|
+
rows = 4,
|
|
13
|
+
maxLength,
|
|
14
|
+
required = false,
|
|
15
|
+
disabled = false,
|
|
16
|
+
variant = "default",
|
|
17
|
+
className = "",
|
|
18
|
+
...props
|
|
19
|
+
}: TextAreaProps) => {
|
|
20
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
21
|
+
const [charCount, setCharCount] = useState(0);
|
|
22
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (typeof value === "string") {
|
|
26
|
+
setCharCount(value.length);
|
|
27
|
+
}
|
|
28
|
+
}, [value]);
|
|
29
|
+
|
|
30
|
+
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
31
|
+
setCharCount(e.target.value.length);
|
|
32
|
+
if (onChange) {
|
|
33
|
+
onChange(e);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const isFloating = variant === "minimal" && (isFocused || Boolean(value));
|
|
38
|
+
|
|
39
|
+
const variantClasses = {
|
|
40
|
+
default: {
|
|
41
|
+
textarea:
|
|
42
|
+
"border border-zinc-300 bg-white text-zinc-900 shadow-sm hover:border-zinc-400 focus:border-black focus:ring-black/20 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100 dark:focus:border-white dark:focus:ring-white/20",
|
|
43
|
+
label: "mb-2 block text-sm font-medium text-zinc-700 dark:text-zinc-300",
|
|
44
|
+
},
|
|
45
|
+
minimal: {
|
|
46
|
+
textarea:
|
|
47
|
+
"border-0 border-b-2 border-zinc-200 bg-transparent px-0 shadow-none focus:border-black dark:border-zinc-800 dark:text-zinc-100 dark:focus:border-white",
|
|
48
|
+
label: twMerge(
|
|
49
|
+
"absolute left-0 pointer-events-none transition-all duration-200",
|
|
50
|
+
isFloating ? "-top-5 text-xs text-black dark:text-white font-semibold" : "top-3 text-sm text-zinc-400"
|
|
51
|
+
),
|
|
52
|
+
},
|
|
53
|
+
glass: {
|
|
54
|
+
textarea:
|
|
55
|
+
"border border-white/40 bg-white/70 text-zinc-900 shadow-sm backdrop-blur hover:border-white/70 focus:border-black focus:ring-black/20 dark:border-white/10 dark:bg-zinc-950/70 dark:text-zinc-100 dark:focus:border-white dark:focus:ring-white/20",
|
|
56
|
+
label: "mb-2 block text-sm font-medium text-zinc-700 dark:text-zinc-300",
|
|
57
|
+
},
|
|
58
|
+
}[variant];
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className={twMerge("relative w-full", className)}>
|
|
62
|
+
{label && variant !== "minimal" && (
|
|
63
|
+
<label className={variantClasses.label}>
|
|
64
|
+
{label}
|
|
65
|
+
{required && <span className="ml-1 text-rose-500">*</span>}
|
|
66
|
+
</label>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
<div className="relative">
|
|
70
|
+
<textarea
|
|
71
|
+
ref={textareaRef}
|
|
72
|
+
value={value}
|
|
73
|
+
onChange={handleChange}
|
|
74
|
+
onFocus={() => setIsFocused(true)}
|
|
75
|
+
onBlur={() => setIsFocused(false)}
|
|
76
|
+
placeholder={variant === "minimal" ? (isFocused ? placeholder : "") : placeholder}
|
|
77
|
+
rows={rows}
|
|
78
|
+
maxLength={maxLength}
|
|
79
|
+
required={required}
|
|
80
|
+
disabled={disabled}
|
|
81
|
+
className={twMerge(
|
|
82
|
+
"w-full resize-none rounded-lg px-3 py-3 text-sm outline-none transition-all duration-200 focus:ring-2 disabled:cursor-not-allowed disabled:bg-zinc-50 dark:disabled:bg-zinc-900 disabled:opacity-60",
|
|
83
|
+
variantClasses.textarea,
|
|
84
|
+
error ? "border-rose-500 focus:border-rose-500 focus:ring-rose-500/20" : ""
|
|
85
|
+
)}
|
|
86
|
+
{...props}
|
|
87
|
+
/>
|
|
88
|
+
|
|
89
|
+
{variant === "minimal" && label && (
|
|
90
|
+
<label
|
|
91
|
+
className={variantClasses.label}
|
|
92
|
+
onClick={() => textareaRef.current?.focus()}
|
|
93
|
+
>
|
|
94
|
+
{label}
|
|
95
|
+
{required && <span className="ml-1 text-rose-500">*</span>}
|
|
96
|
+
</label>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div className="mt-2 flex items-start justify-between gap-3">
|
|
101
|
+
<div className="min-w-0 flex-1">
|
|
102
|
+
{error ? (
|
|
103
|
+
<p className="text-xs font-medium text-rose-600">{error}</p>
|
|
104
|
+
) : helperText ? (
|
|
105
|
+
<p className="text-xs text-zinc-500">{helperText}</p>
|
|
106
|
+
) : null}
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{maxLength && (
|
|
110
|
+
<span
|
|
111
|
+
className={twMerge(
|
|
112
|
+
"shrink-0 text-xs text-zinc-400",
|
|
113
|
+
charCount > maxLength * 0.9 ? "text-amber-600" : "",
|
|
114
|
+
charCount === maxLength ? "text-rose-600" : ""
|
|
115
|
+
)}
|
|
116
|
+
>
|
|
117
|
+
{charCount}/{maxLength}
|
|
118
|
+
</span>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
150
123
|
};
|