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.
Files changed (39) hide show
  1. package/README.md +3 -4
  2. package/index.cjs +1 -0
  3. package/package.json +36 -30
  4. package/source/components/button/index.tsx +41 -24
  5. package/source/components/card/index.tsx +30 -9
  6. package/source/components/checkbox/index.tsx +24 -15
  7. package/source/components/datagrid/context/GridContext.tsx +38 -17
  8. package/source/components/datagrid/createTemplate.tsx +2 -2
  9. package/source/components/datagrid/hooks/usePagination.ts +0 -8
  10. package/source/components/datagrid/hooks/useSearch.ts +3 -3
  11. package/source/components/datagrid/index.tsx +2 -1
  12. package/source/components/datagrid/layout/GridBody.tsx +7 -4
  13. package/source/components/datagrid/layout/GridPagination.tsx +3 -2
  14. package/source/components/datagrid/layout/GridToolbar.tsx +2 -1
  15. package/source/components/datagrid/type.ts +18 -2
  16. package/source/components/datepicker/index.tsx +7 -6
  17. package/source/components/input/index.tsx +141 -143
  18. package/source/components/modal/index.tsx +151 -139
  19. package/source/components/modal/types.ts +18 -16
  20. package/source/components/multiselect/PortalDropDown.tsx +1 -0
  21. package/source/components/multiselect/index.tsx +5 -4
  22. package/source/components/navbar/AuthSection.tsx +5 -5
  23. package/source/components/navbar/NavItemComponent.tsx +2 -2
  24. package/source/components/select/PortalDropDown.tsx +20 -4
  25. package/source/components/select/index.tsx +6 -4
  26. package/source/components/select/types.ts +2 -0
  27. package/source/components/spinner/index.tsx +2 -2
  28. package/source/components/tabs/index.tsx +70 -51
  29. package/source/components/tabs/types.ts +15 -14
  30. package/source/components/textarea/index.tsx +122 -149
  31. package/source/components/toast/ToastComponent.tsx +26 -25
  32. package/source/components/toast/ToastIcon.tsx +8 -8
  33. package/source/components/uploader/ProgressAnimation.tsx +2 -2
  34. package/source/components/uploader/UploadedFilePreview.tsx +1 -1
  35. package/source/components/uploader/index.tsx +1 -1
  36. package/source/components/uploader/uploaderIcon.tsx +1 -1
  37. package/source/globalStyle.ts +74 -35
  38. package/source/icon/iconPaths.ts +6 -0
  39. 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-gray-500" : ""
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-slate-800 text-white px-2 py-1 rounded flex items-center gap-1 text-xs whitespace-nowrap"
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-slate-700 rounded p-0.5"
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-slate-800 text-white px-2 py-1 rounded text-xs whitespace-nowrap">
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-blue-500 flex items-center justify-center text-white">
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-gray-700"
82
- : "text-blue-600 hover:bg-blue-50"
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-blue-600 rounded-md hover:bg-blue-700"
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-medium text-blue-600" : ""}
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-medium text-blue-600" : ""}
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
- useEffect(() => {
9
- if (isVisible && targetRef.current) {
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
- }, [isVisible, targetRef]);
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-gray-100 dark:bg-gray-700" : ""
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-gray-500" : ""
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="relative w-full"
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-600",
15
- fullCircleColor = "border-t-red-400",
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 type { TabsProps } from "./types";
10
-
11
- export function Tabs({
12
- tabs,
13
- defaultTabId,
14
- renderContent,
15
- onChange,
16
- className = "",
17
- }: TabsProps) {
18
- const [activeTabId, setActiveTabId] = useState<string>(
19
- defaultTabId || (tabs.length > 0 ? tabs[0].id : "")
20
- );
21
-
22
- const handleTabChange = (tabId: string) => {
23
- setActiveTabId(tabId);
24
- if (onChange) {
25
- onChange(tabId);
26
- }
27
- };
28
-
29
- return (
30
- <div className={`w-full ${className}`}>
31
- <nav className="flex border-b border-gray-200">
32
- {tabs.map((tab) => (
33
- <button
34
- key={tab.id}
35
- disabled={tab.disabled}
36
- className={`px-4 py-2 text-sm font-medium transition-colors ${
37
- tab.disabled
38
- ? "text-gray-300 cursor-not-allowed"
39
- : activeTabId === tab.id
40
- ? "border-b-2 border-black text-black"
41
- : "text-gray-500 hover:text-gray-700 hover:border-gray-300"
42
- }`}
43
- onClick={() => !tab.disabled && handleTabChange(tab.id)}
44
- >
45
- {tab.label}
46
- </button>
47
- ))}
48
- </nav>
49
- <div className="pt-4">{renderContent(activeTabId)}</div>
50
- </div>
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 type { TextAreaProps } from "./types";
3
-
4
- export const TextArea = ({
5
- label,
6
- placeholder = "Enter your text here...",
7
- value,
8
- onChange,
9
- error,
10
- helperText,
11
- rows = 4,
12
- maxLength,
13
- required = false,
14
- disabled = false,
15
- variant = "default",
16
- className = "",
17
- ...props
18
- }: TextAreaProps) => {
19
- const [isFocused, setIsFocused] = useState(false);
20
- const [charCount, setCharCount] = useState(0);
21
- const textareaRef = useRef(null);
22
-
23
- useEffect(() => {
24
- if (value) {
25
- setCharCount(value.length);
26
- }
27
- }, [value]);
28
-
29
- const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
30
- const newValue = e.target.value;
31
- setCharCount(newValue.length);
32
- if (onChange) {
33
- onChange(e);
34
- }
35
- };
36
-
37
- const handleFocus = () => {
38
- setIsFocused(true);
39
- };
40
-
41
- const handleBlur = () => {
42
- setIsFocused(false);
43
- };
44
-
45
- const getVariantClasses = () => {
46
- switch (variant) {
47
- case "minimal":
48
- return {
49
- container: "relative",
50
- textarea: `w-full px-0 py-3 text-gray-900 placeholder-gray-400 bg-transparent border-0 border-b-2 border-gray-200 resize-none focus:border-blue-500 focus:outline-none ${
51
- disabled ? "opacity-50 cursor-not-allowed" : ""
52
- }`,
53
- label: `absolute left-0 transition-all duration-200 pointer-events-none ${
54
- isFocused || value
55
- ? "-top-6 text-sm text-blue-500"
56
- : "top-3 text-base text-gray-400"
57
- }`,
58
- };
59
- default:
60
- return {
61
- container: "relative",
62
- textarea: `w-full px-4 py-3 text-gray-900 placeholder-gray-400 bg-white border-2 border-gray-200 rounded-lg resize-none focus:border-blue-500 focus:ring-blue-500/20 focus:outline-none transition-all duration-200 shadow-sm hover:border-gray-300 ${
63
- error
64
- ? "border-red-500 focus:border-red-500 focus:ring-red-500/20"
65
- : ""
66
- } ${disabled ? "opacity-50 cursor-not-allowed bg-gray-50" : ""}`,
67
- label: `block text-sm font-medium text-gray-700 mb-2 ${
68
- required ? "after:content-['*'] after:text-red-500 after:ml-1" : ""
69
- }`,
70
- };
71
- }
72
- };
73
-
74
- const variantClasses = getVariantClasses();
75
-
76
- return (
77
- <div className={`${variantClasses.container} ${className}`}>
78
- {label && <label className={variantClasses.label}>{label}</label>}
79
-
80
- <div className="relative">
81
- <textarea
82
- ref={textareaRef}
83
- value={value}
84
- onChange={handleChange}
85
- onFocus={handleFocus}
86
- onBlur={handleBlur}
87
- placeholder={
88
- variant === "minimal" && isFocused
89
- ? placeholder
90
- : variant === "minimal"
91
- ? ""
92
- : placeholder
93
- }
94
- rows={rows}
95
- maxLength={maxLength}
96
- required={required}
97
- disabled={disabled}
98
- className={variantClasses.textarea}
99
- {...props}
100
- />
101
-
102
- {/* Floating label for minimal variant */}
103
- {variant === "minimal" && label && (
104
- <label className={variantClasses.label}>{label}</label>
105
- )}
106
- </div>
107
-
108
- {/* Bottom section with helper text, error, and character count */}
109
- <div className="flex justify-between items-start mt-2">
110
- <div className="flex-1">
111
- {error && (
112
- <p className="text-sm text-red-600 flex items-center">
113
- <svg
114
- className="w-4 h-4 mr-1 flex-shrink-0"
115
- fill="currentColor"
116
- viewBox="0 0 20 20"
117
- >
118
- <path
119
- fillRule="evenodd"
120
- d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
121
- clipRule="evenodd"
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
  };