neogestify-ui-components 1.2.9 → 1.2.10

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neogestify-ui-components",
3
- "version": "1.2.9",
3
+ "version": "1.2.10",
4
4
  "description": "Biblioteca de componentes UI reutilizables con React, Tailwind y SweetAlert",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -30,7 +30,7 @@ export const Select: FC<SelectProps> = ({
30
30
  const selectId = id || `select-${Math.random().toString(36).substring(2, 9)}`;
31
31
 
32
32
  const getVariantClasses = () => {
33
- const baseClasses = 'w-full bg-white dark:bg-gray-700 border rounded-lg text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed transition-colors [&>option]:bg-white [&>option]:dark:bg-gray-800 [&>option]:text-gray-900 [&>option]:dark:text-white [&>option]:py-2 [&>option:checked]:bg-indigo-50 [&>option:checked]:dark:bg-indigo-900/50 [&>option:disabled]:opacity-50 [&>option:disabled]:cursor-not-allowed';
33
+ const baseClasses = 'w-full bg-white dark:bg-gray-700 border rounded-lg text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed transition-colors';
34
34
 
35
35
  if (variant === 'small') {
36
36
  return `${baseClasses} px-2.5 py-1.5 text-sm border-gray-300 dark:border-gray-600`;
@@ -39,6 +39,10 @@ export const Select: FC<SelectProps> = ({
39
39
  return `${baseClasses} px-3 py-2 border-gray-300 dark:border-gray-600 ${error ? 'border-red-300 dark:border-red-600 focus:ring-red-500' : ''}`;
40
40
  };
41
41
 
42
+ const getOptionClasses = (option: Option) => {
43
+ return `bg-white dark:bg-gray-800 text-gray-900 dark:text-white py-2 ${option.disabled ? 'opacity-50 cursor-not-allowed' : ''}`;
44
+ };
45
+
42
46
  const combinedClassName = `${getVariantClasses()} ${className}`.trim();
43
47
 
44
48
  return (
@@ -52,7 +56,7 @@ export const Select: FC<SelectProps> = ({
52
56
  )}
53
57
  <select id={selectId} className={combinedClassName} {...props}>
54
58
  {placeholder && placeholder.trim() && (
55
- <option value="" disabled>
59
+ <option value="" disabled className="bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 py-2">
56
60
  {placeholder}
57
61
  </option>
58
62
  )}
@@ -62,6 +66,7 @@ export const Select: FC<SelectProps> = ({
62
66
  value={option.value}
63
67
  disabled={option.disabled}
64
68
  selected={option.selected}
69
+ className={getOptionClasses(option)}
65
70
  >
66
71
  {option.label}
67
72
  </option>
@@ -3,7 +3,6 @@ export { Input } from './Input';
3
3
  export { TextArea } from './TextArea';
4
4
  export { Form } from './Form';
5
5
  export { Select } from './Select';
6
- export { CustomSelect } from './CustomSelect';
7
6
  export { Table } from './Table';
8
7
  export { Modal, type ModalRef } from './Modal';
9
8
  export { Loading } from './Loading';
@@ -1,324 +0,0 @@
1
- import { type FC, type ReactNode, useState, useRef, useEffect } from 'react';
2
-
3
- interface Option {
4
- value: string | number;
5
- label: string;
6
- disabled?: boolean;
7
- selected?: boolean;
8
- }
9
-
10
- interface CustomSelectProps {
11
- options: Option[];
12
- placeholder?: string;
13
- variant?: 'default' | 'small';
14
- error?: boolean;
15
- helperText?: string;
16
- label?: string | ReactNode;
17
- className?: string;
18
- id?: string;
19
- disabled?: boolean;
20
- value?: string | number;
21
- defaultValue?: string | number;
22
- onChange?: (value: string | number) => void;
23
- name?: string;
24
- required?: boolean;
25
- // Styling props for customization
26
- optionClassName?: string;
27
- optionSelectedClassName?: string;
28
- optionHoverClassName?: string;
29
- optionDisabledClassName?: string;
30
- dropdownClassName?: string;
31
- }
32
-
33
- export const CustomSelect: FC<CustomSelectProps> = ({
34
- options,
35
- placeholder,
36
- variant = 'default',
37
- error = false,
38
- helperText,
39
- label,
40
- className = '',
41
- id,
42
- disabled = false,
43
- value: controlledValue,
44
- defaultValue,
45
- onChange,
46
- name,
47
- required,
48
- optionClassName = '',
49
- optionSelectedClassName = '',
50
- optionHoverClassName = '',
51
- optionDisabledClassName = '',
52
- dropdownClassName = '',
53
- }) => {
54
- const selectId = id || `custom-select-${Math.random().toString(36).substring(2, 9)}`;
55
- const [isOpen, setIsOpen] = useState(false);
56
- const [selectedValue, setSelectedValue] = useState<string | number | undefined>(
57
- controlledValue ?? defaultValue ?? options.find(opt => opt.selected)?.value
58
- );
59
- const [hoveredIndex, setHoveredIndex] = useState<number>(-1);
60
- const [searchTerm, setSearchTerm] = useState('');
61
- const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
62
- const containerRef = useRef<HTMLDivElement>(null);
63
- const dropdownRef = useRef<HTMLDivElement>(null);
64
-
65
- const isControlled = controlledValue !== undefined;
66
- const currentValue = isControlled ? controlledValue : selectedValue;
67
-
68
- useEffect(() => {
69
- if (isControlled) {
70
- setSelectedValue(controlledValue);
71
- }
72
- }, [controlledValue, isControlled]);
73
-
74
- useEffect(() => {
75
- const handleClickOutside = (event: MouseEvent) => {
76
- if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
77
- setIsOpen(false);
78
- setSearchTerm('');
79
- }
80
- };
81
-
82
- if (isOpen) {
83
- document.addEventListener('mousedown', handleClickOutside);
84
- return () => document.removeEventListener('mousedown', handleClickOutside);
85
- }
86
- }, [isOpen]);
87
-
88
- useEffect(() => {
89
- if (isOpen && hoveredIndex >= 0 && dropdownRef.current) {
90
- const hoveredElement = dropdownRef.current.children[hoveredIndex] as HTMLElement;
91
- if (hoveredElement) {
92
- hoveredElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
93
- }
94
- }
95
- }, [hoveredIndex, isOpen]);
96
-
97
- const selectedOption = options.find(opt => opt.value === currentValue);
98
-
99
- const handleSelect = (option: Option) => {
100
- if (option.disabled) return;
101
-
102
- if (!isControlled) {
103
- setSelectedValue(option.value);
104
- }
105
-
106
- onChange?.(option.value);
107
- setIsOpen(false);
108
- setSearchTerm('');
109
- setHoveredIndex(-1);
110
- };
111
-
112
- const handleKeyDown = (e: React.KeyboardEvent) => {
113
- if (disabled) return;
114
-
115
- switch (e.key) {
116
- case 'Enter':
117
- case ' ':
118
- e.preventDefault();
119
- if (!isOpen) {
120
- setIsOpen(true);
121
- const selectedIdx = options.findIndex(opt => opt.value === currentValue);
122
- setHoveredIndex(selectedIdx >= 0 ? selectedIdx : 0);
123
- } else if (hoveredIndex >= 0) {
124
- handleSelect(options[hoveredIndex]);
125
- }
126
- break;
127
-
128
- case 'ArrowDown':
129
- e.preventDefault();
130
- if (!isOpen) {
131
- setIsOpen(true);
132
- const selectedIdx = options.findIndex(opt => opt.value === currentValue);
133
- setHoveredIndex(selectedIdx >= 0 ? selectedIdx : 0);
134
- } else {
135
- setHoveredIndex(prev => {
136
- let next = prev + 1;
137
- while (next < options.length && options[next].disabled) {
138
- next++;
139
- }
140
- return next < options.length ? next : prev;
141
- });
142
- }
143
- break;
144
-
145
- case 'ArrowUp':
146
- e.preventDefault();
147
- if (isOpen) {
148
- setHoveredIndex(prev => {
149
- let next = prev - 1;
150
- while (next >= 0 && options[next].disabled) {
151
- next--;
152
- }
153
- return next >= 0 ? next : prev;
154
- });
155
- }
156
- break;
157
-
158
- case 'Escape':
159
- e.preventDefault();
160
- setIsOpen(false);
161
- setSearchTerm('');
162
- break;
163
-
164
- case 'Tab':
165
- if (isOpen) {
166
- setIsOpen(false);
167
- setSearchTerm('');
168
- }
169
- break;
170
-
171
- default:
172
- // Type-ahead search
173
- if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
174
- e.preventDefault();
175
- const newSearchTerm = searchTerm + e.key.toLowerCase();
176
- setSearchTerm(newSearchTerm);
177
-
178
- const matchIndex = options.findIndex(opt =>
179
- !opt.disabled && opt.label.toLowerCase().startsWith(newSearchTerm)
180
- );
181
-
182
- if (matchIndex >= 0) {
183
- setHoveredIndex(matchIndex);
184
- if (!isOpen) {
185
- handleSelect(options[matchIndex]);
186
- }
187
- }
188
-
189
- if (searchTimeout) clearTimeout(searchTimeout);
190
- const timeout = setTimeout(() => setSearchTerm(''), 1000);
191
- setSearchTimeout(timeout);
192
- }
193
- break;
194
- }
195
- };
196
-
197
- const getButtonClasses = () => {
198
- const baseClasses = 'w-full bg-white dark:bg-gray-700 border rounded-lg text-left flex items-center justify-between focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed transition-colors';
199
-
200
- if (variant === 'small') {
201
- return `${baseClasses} px-2.5 py-1.5 text-sm border-gray-300 dark:border-gray-600`;
202
- }
203
-
204
- return `${baseClasses} px-3 py-2 border-gray-300 dark:border-gray-600 ${
205
- error ? 'border-red-300 dark:border-red-600 focus:ring-red-500' : ''
206
- }`;
207
- };
208
-
209
- const getOptionClasses = (option: Option, index: number) => {
210
- const isSelected = option.value === currentValue;
211
- const isHovered = index === hoveredIndex;
212
-
213
- let classes = 'px-3 py-2 cursor-pointer transition-colors';
214
-
215
- if (option.disabled) {
216
- classes += ' opacity-50 cursor-not-allowed bg-gray-100 dark:bg-gray-800';
217
- if (optionDisabledClassName) {
218
- classes += ` ${optionDisabledClassName}`;
219
- }
220
- } else {
221
- if (isSelected) {
222
- classes += ' bg-indigo-50 dark:bg-indigo-900/50 text-indigo-900 dark:text-indigo-100 font-medium';
223
- if (optionSelectedClassName) {
224
- classes += ` ${optionSelectedClassName}`;
225
- }
226
- } else if (isHovered) {
227
- classes += ' bg-gray-100 dark:bg-gray-600';
228
- if (optionHoverClassName) {
229
- classes += ` ${optionHoverClassName}`;
230
- }
231
- } else {
232
- classes += ' text-gray-900 dark:text-white hover:bg-gray-100 dark:hover:bg-gray-600';
233
- }
234
- }
235
-
236
- if (optionClassName) {
237
- classes += ` ${optionClassName}`;
238
- }
239
-
240
- return classes;
241
- };
242
-
243
- const combinedButtonClassName = `${getButtonClasses()} ${className}`.trim();
244
- const combinedDropdownClassName = `absolute z-50 w-full mt-1 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-60 overflow-auto ${dropdownClassName}`.trim();
245
-
246
- return (
247
- <div className="space-y-1 w-full">
248
- {label && typeof label === 'string' ? (
249
- <label htmlFor={selectId} className="block text-xs font-normal text-gray-700 dark:text-gray-300">
250
- {label}
251
- {required && <span className="text-red-500 ml-1">*</span>}
252
- </label>
253
- ) : (
254
- label
255
- )}
256
-
257
- <div ref={containerRef} className="relative w-full">
258
- <button
259
- id={selectId}
260
- type="button"
261
- role="combobox"
262
- aria-expanded={isOpen}
263
- aria-haspopup="listbox"
264
- aria-labelledby={label ? `${selectId}-label` : undefined}
265
- aria-required={required}
266
- disabled={disabled}
267
- className={combinedButtonClassName}
268
- onClick={() => !disabled && setIsOpen(!isOpen)}
269
- onKeyDown={handleKeyDown}
270
- >
271
- <span className={selectedOption ? 'text-gray-900 dark:text-white' : 'text-gray-500 dark:text-gray-400'}>
272
- {selectedOption ? selectedOption.label : placeholder || 'Seleccionar...'}
273
- </span>
274
- <svg
275
- className={`w-5 h-5 transition-transform text-gray-400 ${isOpen ? 'transform rotate-180' : ''}`}
276
- fill="none"
277
- stroke="currentColor"
278
- viewBox="0 0 24 24"
279
- >
280
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
281
- </svg>
282
- </button>
283
-
284
- {isOpen && (
285
- <div
286
- ref={dropdownRef}
287
- role="listbox"
288
- aria-labelledby={label ? `${selectId}-label` : undefined}
289
- className={combinedDropdownClassName}
290
- >
291
- {options.map((option, index) => (
292
- <div
293
- key={option.value}
294
- role="option"
295
- aria-selected={option.value === currentValue}
296
- aria-disabled={option.disabled}
297
- className={getOptionClasses(option, index)}
298
- onClick={() => handleSelect(option)}
299
- onMouseEnter={() => !option.disabled && setHoveredIndex(index)}
300
- >
301
- {option.label}
302
- </div>
303
- ))}
304
- </div>
305
- )}
306
-
307
- {/* Hidden input for form submission */}
308
- {name && (
309
- <input
310
- type="hidden"
311
- name={name}
312
- value={currentValue ?? ''}
313
- />
314
- )}
315
- </div>
316
-
317
- {helperText && (
318
- <p className={`mt-1 text-sm ${error ? 'text-red-600 dark:text-red-400' : 'text-gray-600 dark:text-gray-400'}`}>
319
- {helperText}
320
- </p>
321
- )}
322
- </div>
323
- );
324
- };