siesa-ui-kit 1.0.5 → 1.0.6
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/dist/index.cjs +1479 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +1479 -0
- package/dist/index.js.map +1 -0
- package/package.json +23 -14
- package/claude/agents/siesa-ui-kit-specialist.md +0 -2401
- package/claude/prompts/component-template.md +0 -121
- package/claude/settings.local.json +0 -61
- package/docs/border-radius.md +0 -1261
- package/docs/colors.md +0 -832
- package/docs/dark-mode-guide.md +0 -1426
- package/docs/filters.md +0 -1243
- package/docs/icons.md +0 -1283
- package/docs/shadows.md +0 -1377
- package/docs/spacing.md +0 -1684
- package/docs/typography.md +0 -1268
- package/postcss.config.cjs +0 -6
- package/src/App.css +0 -42
- package/src/App.tsx +0 -8
- package/src/ButtonTest.tsx +0 -147
- package/src/assets/fonts/README.md +0 -261
- package/src/assets/fonts/SiesaBT/SiesaBT-Bold.otf +0 -0
- package/src/assets/fonts/SiesaBT/SiesaBT-Light.otf +0 -0
- package/src/assets/fonts/SiesaBT/SiesaBT-Regular.otf +0 -0
- package/src/assets/react.svg +0 -1
- package/src/components/Alert/Alert.stories.tsx +0 -332
- package/src/components/Alert/Alert.tsx +0 -106
- package/src/components/Alert/Alert.types.ts +0 -54
- package/src/components/Avatar/Avatar.stories.tsx +0 -494
- package/src/components/Avatar/Avatar.tsx +0 -143
- package/src/components/Avatar/Avatar.types.ts +0 -53
- package/src/components/Badge/Badge.stories.tsx +0 -339
- package/src/components/Badge/Badge.tsx +0 -278
- package/src/components/Badge/Badge.types.ts +0 -58
- package/src/components/Button/Button.stories.tsx +0 -950
- package/src/components/Button/Button.tsx +0 -337
- package/src/components/Button/Button.types.ts +0 -180
- package/src/components/Button/icons.tsx +0 -87
- package/src/components/Button/index.ts +0 -3
- package/src/components/Checkbox/Checkbox.stories.tsx +0 -453
- package/src/components/Checkbox/Checkbox.tsx +0 -208
- package/src/components/Checkbox/Checkbox.types.ts +0 -61
- package/src/components/DescriptionList/DescriptionList.stories.tsx +0 -250
- package/src/components/DescriptionList/DescriptionList.tsx +0 -96
- package/src/components/DescriptionList/DescriptionList.types.ts +0 -29
- package/src/components/Divider/Divider.stories.tsx +0 -263
- package/src/components/Divider/Divider.tsx +0 -80
- package/src/components/Divider/Divider.types.ts +0 -24
- package/src/components/Dropdown/Dropdown.stories.tsx +0 -552
- package/src/components/Dropdown/Dropdown.tsx +0 -422
- package/src/components/Dropdown/Dropdown.types.ts +0 -146
- package/src/components/Dropdown/README.md +0 -266
- package/src/components/Dropdown/icons.tsx +0 -72
- package/src/components/Dropdown/index.ts +0 -8
- package/src/components/Input/Input.stories.tsx +0 -583
- package/src/components/Input/Input.tsx +0 -204
- package/src/components/Input/Input.types.ts +0 -80
- package/src/components/Input/icons.tsx +0 -145
- package/src/components/Input/index.ts +0 -2
- package/src/components/LoginView/LoginView.stories.tsx +0 -148
- package/src/components/LoginView/LoginView.tsx +0 -426
- package/src/components/LoginView/LoginView.types.ts +0 -52
- package/src/components/LoginView/README.md +0 -396
- package/src/components/LoginView/icons.tsx +0 -85
- package/src/components/LoginView/index.ts +0 -3
- package/src/components/Navbar/Navbar.stories.tsx +0 -810
- package/src/components/Navbar/Navbar.tsx +0 -755
- package/src/components/Navbar/Navbar.types.ts +0 -219
- package/src/components/Navbar/README.md +0 -279
- package/src/components/Navbar/icons.tsx +0 -102
- package/src/components/Navbar/index.ts +0 -8
- package/src/components/NavigationBar/NavigationBar.stories.tsx +0 -406
- package/src/components/NavigationBar/NavigationBar.tsx +0 -246
- package/src/components/NavigationBar/NavigationBar.types.ts +0 -74
- package/src/components/NavigationBar/README.md +0 -469
- package/src/components/NavigationBar/index.ts +0 -2
- package/src/components/NavigationRail/NavigationRail.stories.tsx +0 -417
- package/src/components/NavigationRail/NavigationRail.tsx +0 -418
- package/src/components/NavigationRail/NavigationRail.types.ts +0 -109
- package/src/components/NavigationRail/README.md +0 -224
- package/src/components/NavigationRail/index.ts +0 -2
- package/src/components/Notification/Notification.stories.tsx +0 -513
- package/src/components/Notification/Notification.tsx +0 -145
- package/src/components/Notification/Notification.types.ts +0 -142
- package/src/components/Notification/README.md +0 -409
- package/src/components/Notification/index.ts +0 -3
- package/src/components/POSConvention/POSConvention.stories.tsx +0 -235
- package/src/components/POSConvention/POSConvention.tsx +0 -129
- package/src/components/POSConvention/POSConvention.types.ts +0 -38
- package/src/components/POSConvention/README.md +0 -123
- package/src/components/POSConvention/icons.tsx +0 -45
- package/src/components/POSConvention/index.ts +0 -3
- package/src/components/POSLocationButton/POSLocationButton.stories.tsx +0 -531
- package/src/components/POSLocationButton/POSLocationButton.tsx +0 -247
- package/src/components/POSLocationButton/POSLocationButton.types.ts +0 -87
- package/src/components/POSLocationButton/README.md +0 -253
- package/src/components/POSLocationButton/icons.tsx +0 -120
- package/src/components/POSLocationButton/index.ts +0 -14
- package/src/components/POSNumberButton/POSNumberButton.stories.tsx +0 -415
- package/src/components/POSNumberButton/POSNumberButton.tsx +0 -179
- package/src/components/POSNumberButton/POSNumberButton.types.ts +0 -51
- package/src/components/POSNumberButton/README.md +0 -321
- package/src/components/POSNumberButton/index.ts +0 -3
- package/src/components/POSProductButton/POSProductButton.stories.tsx +0 -318
- package/src/components/POSProductButton/POSProductButton.tsx +0 -152
- package/src/components/POSProductButton/POSProductButton.types.ts +0 -46
- package/src/components/POSProductButton/README.md +0 -269
- package/src/components/POSProductButton/index.ts +0 -2
- package/src/components/POSProductCard/POSProductCard.stories.tsx +0 -642
- package/src/components/POSProductCard/POSProductCard.tsx +0 -208
- package/src/components/POSProductCard/POSProductCard.types.ts +0 -76
- package/src/components/POSProductCard/README.md +0 -179
- package/src/components/POSProductCard/icons.tsx +0 -26
- package/src/components/POSProductCard/index.ts +0 -2
- package/src/components/POSProductSidebarItems/POSProductSidebarItems.stories.tsx +0 -753
- package/src/components/POSProductSidebarItems/POSProductSidebarItems.tsx +0 -332
- package/src/components/POSProductSidebarItems/POSProductSidebarItems.types.ts +0 -119
- package/src/components/POSProductSidebarItems/README.md +0 -198
- package/src/components/POSProductSidebarItems/icons.tsx +0 -21
- package/src/components/POSProductSidebarItems/index.ts +0 -3
- package/src/components/POSTable/POSTable.stories.tsx +0 -737
- package/src/components/POSTable/POSTable.tsx +0 -401
- package/src/components/POSTable/POSTable.types.ts +0 -83
- package/src/components/POSTable/README.md +0 -286
- package/src/components/POSTable/index.ts +0 -7
- package/src/components/Pagination/Pagination.stories.tsx +0 -555
- package/src/components/Pagination/Pagination.tsx +0 -286
- package/src/components/Pagination/Pagination.types.ts +0 -93
- package/src/components/Pagination/README.md +0 -298
- package/src/components/Pagination/icons.tsx +0 -47
- package/src/components/Pagination/index.ts +0 -3
- package/src/components/Quantity/Quantity.stories.tsx +0 -457
- package/src/components/Quantity/Quantity.tsx +0 -289
- package/src/components/Quantity/Quantity.types.ts +0 -70
- package/src/components/Radio/Radio.stories.tsx +0 -523
- package/src/components/Radio/Radio.tsx +0 -170
- package/src/components/Radio/Radio.types.ts +0 -122
- package/src/components/Select/README.md +0 -299
- package/src/components/Select/Select.stories.tsx +0 -673
- package/src/components/Select/Select.tsx +0 -454
- package/src/components/Select/Select.types.ts +0 -148
- package/src/components/Select/icons.tsx +0 -50
- package/src/components/Select/index.ts +0 -3
- package/src/components/SignUpView/SignUpView.stories.tsx +0 -129
- package/src/components/SignUpView/SignUpView.tsx +0 -503
- package/src/components/SignUpView/SignUpView.types.ts +0 -58
- package/src/components/SignUpView/icons.tsx +0 -71
- package/src/components/SignUpView/index.ts +0 -3
- package/src/components/Switch/README.md +0 -112
- package/src/components/Switch/Switch.stories.tsx +0 -550
- package/src/components/Switch/Switch.tsx +0 -246
- package/src/components/Switch/Switch.types.ts +0 -67
- package/src/components/Table/README.md +0 -369
- package/src/components/Table/Table.stories.tsx +0 -805
- package/src/components/Table/Table.tsx +0 -688
- package/src/components/Table/Table.types.ts +0 -204
- package/src/components/Table/index.ts +0 -9
- package/src/components/Tabs/README.md +0 -201
- package/src/components/Tabs/Tabs.stories.tsx +0 -580
- package/src/components/Tabs/Tabs.tsx +0 -356
- package/src/components/Tabs/Tabs.types.ts +0 -127
- package/src/components/Tabs/icons.tsx +0 -129
- package/src/components/Tabs/index.ts +0 -11
- package/src/components/Textarea/Textarea.stories.tsx +0 -535
- package/src/components/Textarea/Textarea.tsx +0 -188
- package/src/components/Textarea/Textarea.types.ts +0 -54
- package/src/context/ThemeContext.tsx +0 -99
- package/src/context/index.ts +0 -1
- package/src/index.css +0 -29
- package/src/index.ts +0 -39
- package/src/main.tsx +0 -10
- package/src/views/ProductsView/ProductsView.stories.tsx +0 -344
- package/src/views/ProductsView/ProductsView.tsx +0 -480
- package/src/views/ProductsView/ProductsView.types.ts +0 -238
- package/src/views/ProductsView/README.md +0 -312
- package/src/views/ProductsView/icons.tsx +0 -38
- package/src/views/ProductsView/index.ts +0 -8
- package/src/views/RecoverPasswordView/README.md +0 -269
- package/src/views/RecoverPasswordView/RecoverPasswordView.stories.tsx +0 -131
- package/src/views/RecoverPasswordView/RecoverPasswordView.tsx +0 -376
- package/src/views/RecoverPasswordView/RecoverPasswordView.types.ts +0 -56
- package/src/views/RecoverPasswordView/icons.tsx +0 -17
- package/src/views/RecoverPasswordView/index.ts +0 -2
- package/src/views/TableLayoutView/README.md +0 -268
- package/src/views/TableLayoutView/TableLayoutView.stories.tsx +0 -235
- package/src/views/TableLayoutView/TableLayoutView.tsx +0 -461
- package/src/views/TableLayoutView/TableLayoutView.types.ts +0 -209
- package/src/views/TableLayoutView/icons.tsx +0 -113
- package/src/views/TableLayoutView/index.ts +0 -6
- package/storybook/main.ts +0 -20
- package/storybook/preview.tsx +0 -84
- package/storybook/vitest.setup.ts +0 -7
- package/tailwind.config.js +0 -128
- /package/{public → dist}/,Business Logo.png +0 -0
- /package/{public → dist}/.Siesa Logo.png +0 -0
- /package/{public → dist}/bg_siesa.png +0 -0
- /package/{public → dist}/siesa_logo_mobile.png +0 -0
- /package/{public → dist}/vite.svg +0 -0
|
@@ -1,454 +0,0 @@
|
|
|
1
|
-
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
-
import type { SelectProps, SelectOption } from './Select.types';
|
|
3
|
-
import { ChevronUpDownIcon, CheckIcon } from './icons';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Select - Componente de selección (dropdown) del sistema de diseño Siesa
|
|
7
|
-
*
|
|
8
|
-
* Componente Select personalizado con menú desplegable para seleccionar opciones
|
|
9
|
-
* de una lista. Incluye navegación por teclado, click outside y dark mode completo.
|
|
10
|
-
*
|
|
11
|
-
* Mejores prácticas implementadas:
|
|
12
|
-
* - Orden de modificadores: {responsive}:{dark}:{state}:{utility}
|
|
13
|
-
* - Dark mode con estrategia 'class' (darkMode: 'class')
|
|
14
|
-
* - Tokens de color consistentes con la documentación
|
|
15
|
-
* - Type safety con TypeScript estricto
|
|
16
|
-
* - Accesibilidad con ARIA labels y keyboard navigation
|
|
17
|
-
* - Click outside para cerrar el menú
|
|
18
|
-
* - Soporte de teclado (Enter, Escape, Arrow Up/Down)
|
|
19
|
-
*
|
|
20
|
-
* @see docs/colors.md - Sistema de colores
|
|
21
|
-
* @see docs/typography.md - Sistema tipográfico
|
|
22
|
-
* @see docs/spacing.md - Sistema de espaciado
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* ```tsx
|
|
26
|
-
* <Select
|
|
27
|
-
* options={[
|
|
28
|
-
* { value: '1', label: 'Option 1' },
|
|
29
|
-
* { value: '2', label: 'Option 2' },
|
|
30
|
-
* ]}
|
|
31
|
-
* value={selectedValue}
|
|
32
|
-
* onChange={setSelectedValue}
|
|
33
|
-
* placeholder="Select an option"
|
|
34
|
-
* label="Choose one"
|
|
35
|
-
* />
|
|
36
|
-
* ```
|
|
37
|
-
*/
|
|
38
|
-
export const Select: React.FC<SelectProps> = ({
|
|
39
|
-
options = [],
|
|
40
|
-
value,
|
|
41
|
-
defaultValue,
|
|
42
|
-
placeholder = 'Seleccionar...',
|
|
43
|
-
disabled = false,
|
|
44
|
-
error = false,
|
|
45
|
-
label,
|
|
46
|
-
description,
|
|
47
|
-
showLabel = true,
|
|
48
|
-
showDescription = true,
|
|
49
|
-
menuHeader,
|
|
50
|
-
onChange,
|
|
51
|
-
className = '',
|
|
52
|
-
triggerClassName = '',
|
|
53
|
-
menuClassName = '',
|
|
54
|
-
ariaLabel,
|
|
55
|
-
id,
|
|
56
|
-
name,
|
|
57
|
-
required = false,
|
|
58
|
-
menuPosition = 'bottom',
|
|
59
|
-
fullWidth = false,
|
|
60
|
-
}) => {
|
|
61
|
-
// ===== ESTADO Y REFS =====
|
|
62
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
63
|
-
const [selectedValue, setSelectedValue] = useState<string | number | undefined>(
|
|
64
|
-
value !== undefined ? value : defaultValue
|
|
65
|
-
);
|
|
66
|
-
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
67
|
-
|
|
68
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
69
|
-
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
70
|
-
const menuRef = useRef<HTMLDivElement>(null);
|
|
71
|
-
|
|
72
|
-
// ===== SINCRONIZAR VALOR CONTROLADO =====
|
|
73
|
-
useEffect(() => {
|
|
74
|
-
if (value !== undefined) {
|
|
75
|
-
setSelectedValue(value);
|
|
76
|
-
}
|
|
77
|
-
}, [value]);
|
|
78
|
-
|
|
79
|
-
// ===== CERRAR AL HACER CLICK FUERA =====
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
const handleClickOutside = (event: MouseEvent) => {
|
|
82
|
-
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
83
|
-
setIsOpen(false);
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
if (isOpen) {
|
|
88
|
-
document.addEventListener('mousedown', handleClickOutside);
|
|
89
|
-
return () => {
|
|
90
|
-
document.removeEventListener('mousedown', handleClickOutside);
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
}, [isOpen]);
|
|
94
|
-
|
|
95
|
-
// ===== KEYBOARD NAVIGATION =====
|
|
96
|
-
useEffect(() => {
|
|
97
|
-
const handleKeyDown = (event: KeyboardEvent) => {
|
|
98
|
-
if (!isOpen) return;
|
|
99
|
-
|
|
100
|
-
switch (event.key) {
|
|
101
|
-
case 'Escape':
|
|
102
|
-
setIsOpen(false);
|
|
103
|
-
triggerRef.current?.focus();
|
|
104
|
-
break;
|
|
105
|
-
case 'ArrowDown':
|
|
106
|
-
event.preventDefault();
|
|
107
|
-
setFocusedIndex((prev) => (prev < options.length - 1 ? prev + 1 : 0));
|
|
108
|
-
break;
|
|
109
|
-
case 'ArrowUp':
|
|
110
|
-
event.preventDefault();
|
|
111
|
-
setFocusedIndex((prev) => (prev > 0 ? prev - 1 : options.length - 1));
|
|
112
|
-
break;
|
|
113
|
-
case 'Enter':
|
|
114
|
-
event.preventDefault();
|
|
115
|
-
if (focusedIndex >= 0 && focusedIndex < options.length) {
|
|
116
|
-
handleSelect(options[focusedIndex]);
|
|
117
|
-
}
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
if (isOpen) {
|
|
123
|
-
document.addEventListener('keydown', handleKeyDown);
|
|
124
|
-
return () => {
|
|
125
|
-
document.removeEventListener('keydown', handleKeyDown);
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
}, [isOpen, focusedIndex, options]);
|
|
129
|
-
|
|
130
|
-
// ===== HELPERS =====
|
|
131
|
-
const selectedOption = options.find((opt) => opt.value === selectedValue);
|
|
132
|
-
|
|
133
|
-
const toggleMenu = () => {
|
|
134
|
-
if (!disabled) {
|
|
135
|
-
setIsOpen(!isOpen);
|
|
136
|
-
setFocusedIndex(-1);
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
const handleSelect = (option: SelectOption) => {
|
|
141
|
-
if (option.disabled) return;
|
|
142
|
-
|
|
143
|
-
setSelectedValue(option.value);
|
|
144
|
-
setIsOpen(false);
|
|
145
|
-
onChange?.(option.value);
|
|
146
|
-
|
|
147
|
-
setTimeout(() => {
|
|
148
|
-
triggerRef.current?.focus();
|
|
149
|
-
}, 0);
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
// ===== CLASES BASE DEL TRIGGER =====
|
|
153
|
-
// Usando el sistema tipográfico Paragraph Regular (14px = text-sm)
|
|
154
|
-
// Border radius rounded-lg (8px) consistente con especificaciones
|
|
155
|
-
// Gap de 12px (gap-3) entre texto e icono según Figma
|
|
156
|
-
const baseTriggerClasses = `
|
|
157
|
-
inline-flex
|
|
158
|
-
items-center
|
|
159
|
-
justify-between
|
|
160
|
-
gap-3
|
|
161
|
-
w-full
|
|
162
|
-
px-3
|
|
163
|
-
py-2
|
|
164
|
-
text-sm
|
|
165
|
-
font-normal
|
|
166
|
-
leading-5
|
|
167
|
-
rounded-lg
|
|
168
|
-
border
|
|
169
|
-
transition-all
|
|
170
|
-
duration-150
|
|
171
|
-
`;
|
|
172
|
-
|
|
173
|
-
// ===== CLASES DE WIDTH =====
|
|
174
|
-
const widthClasses = fullWidth ? 'w-full' : 'min-w-[200px]';
|
|
175
|
-
|
|
176
|
-
// ===== CLASES PARA ESTADOS DEL TRIGGER =====
|
|
177
|
-
// Orden de modificadores: {responsive}:{dark}:{state}:{utility}
|
|
178
|
-
const triggerStateClasses = error
|
|
179
|
-
? `
|
|
180
|
-
border-error-border
|
|
181
|
-
bg-error-bg
|
|
182
|
-
text-content-primary
|
|
183
|
-
dark:border-error-border
|
|
184
|
-
dark:bg-error-bg
|
|
185
|
-
dark:text-dark-content-primary
|
|
186
|
-
`
|
|
187
|
-
: `
|
|
188
|
-
bg-bg-primary
|
|
189
|
-
border-border-primary
|
|
190
|
-
text-content-primary
|
|
191
|
-
dark:bg-dark-bg-primary
|
|
192
|
-
dark:border-dark-border-primary
|
|
193
|
-
dark:text-dark-content-primary
|
|
194
|
-
`;
|
|
195
|
-
|
|
196
|
-
// ===== CLASES PARA HOVER DEL TRIGGER =====
|
|
197
|
-
// Hover overlay rgba(0,0,0,0.024) según Figma
|
|
198
|
-
const triggerHoverClasses =
|
|
199
|
-
!disabled && !error
|
|
200
|
-
? `
|
|
201
|
-
hover:bg-[rgba(255,255,255,1)]
|
|
202
|
-
hover:bg-[linear-gradient(90deg,rgba(0,0,0,0.024)_0%,rgba(0,0,0,0.024)_100%)]
|
|
203
|
-
dark:hover:bg-[rgba(17,45,87,1)]
|
|
204
|
-
dark:hover:bg-[linear-gradient(90deg,rgba(255,255,255,0.1)_0%,rgba(255,255,255,0.1)_100%)]
|
|
205
|
-
`
|
|
206
|
-
: '';
|
|
207
|
-
|
|
208
|
-
// ===== CLASES PARA FOCUS DEL TRIGGER =====
|
|
209
|
-
// Focus rings adaptativos siguiendo el patrón de otros componentes
|
|
210
|
-
const triggerFocusClasses = !disabled
|
|
211
|
-
? `
|
|
212
|
-
focus:outline-none
|
|
213
|
-
focus:ring-2
|
|
214
|
-
focus:ring-primary-custom-400
|
|
215
|
-
focus:ring-offset-2
|
|
216
|
-
dark:focus:ring-dark-border-custom
|
|
217
|
-
dark:focus:ring-offset-dark-bg-primary
|
|
218
|
-
`
|
|
219
|
-
: '';
|
|
220
|
-
|
|
221
|
-
// ===== CLASES PARA DISABLED DEL TRIGGER =====
|
|
222
|
-
const triggerDisabledClasses = disabled
|
|
223
|
-
? `
|
|
224
|
-
opacity-50
|
|
225
|
-
cursor-not-allowed
|
|
226
|
-
`
|
|
227
|
-
: '';
|
|
228
|
-
|
|
229
|
-
// ===== COMBINAR CLASES DEL TRIGGER =====
|
|
230
|
-
const finalTriggerClasses = [
|
|
231
|
-
baseTriggerClasses,
|
|
232
|
-
widthClasses,
|
|
233
|
-
triggerStateClasses,
|
|
234
|
-
triggerHoverClasses,
|
|
235
|
-
triggerFocusClasses,
|
|
236
|
-
triggerDisabledClasses,
|
|
237
|
-
triggerClassName,
|
|
238
|
-
]
|
|
239
|
-
.join(' ')
|
|
240
|
-
.replace(/\s+/g, ' ')
|
|
241
|
-
.trim();
|
|
242
|
-
|
|
243
|
-
// ===== CLASES BASE DEL MENU =====
|
|
244
|
-
// Según Figma: altura fija 160px (h-40), sin borde visible (border-0),
|
|
245
|
-
// spacing de 4px desde trigger (mt-1), sin shadow prominente
|
|
246
|
-
const baseMenuClasses = `
|
|
247
|
-
absolute
|
|
248
|
-
z-50
|
|
249
|
-
w-full
|
|
250
|
-
min-w-[200px]
|
|
251
|
-
max-h-40
|
|
252
|
-
overflow-y-auto
|
|
253
|
-
p-1
|
|
254
|
-
rounded-md
|
|
255
|
-
border-0
|
|
256
|
-
transition-all
|
|
257
|
-
duration-150
|
|
258
|
-
`;
|
|
259
|
-
|
|
260
|
-
// ===== CLASES DE POSICIÓN DEL MENU =====
|
|
261
|
-
// Figma muestra 4px de separación, usando mt-1 en lugar de mt-2
|
|
262
|
-
const menuPositionClasses = menuPosition === 'top' ? 'bottom-full mb-1' : 'top-full mt-1';
|
|
263
|
-
|
|
264
|
-
// ===== CLASES DE COLOR DEL MENU =====
|
|
265
|
-
// Background secundario según Figma, sin borde prominente
|
|
266
|
-
const menuColorClasses = `
|
|
267
|
-
bg-background-secondary
|
|
268
|
-
dark:bg-dark-bg-primary
|
|
269
|
-
`;
|
|
270
|
-
|
|
271
|
-
// ===== CLASES DE VISIBILIDAD DEL MENU =====
|
|
272
|
-
const menuVisibilityClasses = isOpen ? 'opacity-100 visible' : 'opacity-0 invisible';
|
|
273
|
-
|
|
274
|
-
// ===== COMBINAR CLASES DEL MENU =====
|
|
275
|
-
const finalMenuClasses = [
|
|
276
|
-
baseMenuClasses,
|
|
277
|
-
menuPositionClasses,
|
|
278
|
-
menuColorClasses,
|
|
279
|
-
menuVisibilityClasses,
|
|
280
|
-
menuClassName,
|
|
281
|
-
]
|
|
282
|
-
.join(' ')
|
|
283
|
-
.replace(/\s+/g, ' ')
|
|
284
|
-
.trim();
|
|
285
|
-
|
|
286
|
-
// ===== CLASES BASE DEL MENU ITEM =====
|
|
287
|
-
// CRÍTICO: Figma especifica text-[10px] leading-[12px] (Label XXSmall)
|
|
288
|
-
// NO usar text-xs que es 12px - debe ser exactamente 10px según diseño
|
|
289
|
-
const baseMenuItemClasses = `
|
|
290
|
-
flex
|
|
291
|
-
items-center
|
|
292
|
-
cursor-pointer
|
|
293
|
-
rounded-md
|
|
294
|
-
transition-all
|
|
295
|
-
duration-150
|
|
296
|
-
pl-2
|
|
297
|
-
pr-2.5
|
|
298
|
-
py-0.5
|
|
299
|
-
gap-0.5
|
|
300
|
-
text-[10px]
|
|
301
|
-
leading-[12px]
|
|
302
|
-
font-bold
|
|
303
|
-
`;
|
|
304
|
-
|
|
305
|
-
// ===== FUNCIÓN PARA OBTENER CLASES DE MENU ITEM =====
|
|
306
|
-
const getMenuItemClasses = (option: SelectOption, index: number) => {
|
|
307
|
-
const isSelected = option.value === selectedValue;
|
|
308
|
-
const isFocused = index === focusedIndex;
|
|
309
|
-
|
|
310
|
-
// Estados de color
|
|
311
|
-
const colorClasses =
|
|
312
|
-
isSelected && !isFocused
|
|
313
|
-
? 'text-content-primary dark:text-dark-content-primary'
|
|
314
|
-
: isFocused || (isSelected && isFocused)
|
|
315
|
-
? 'bg-primary-custom-600 text-primary-inverse-content dark:bg-primary-custom-600 dark:text-primary-inverse-content'
|
|
316
|
-
: 'text-content-primary dark:text-dark-content-primary';
|
|
317
|
-
|
|
318
|
-
// Hover
|
|
319
|
-
const hoverClasses = !option.disabled
|
|
320
|
-
? 'hover:bg-primary-custom-600 hover:text-primary-inverse-content dark:hover:bg-primary-custom-600 dark:hover:text-primary-inverse-content'
|
|
321
|
-
: '';
|
|
322
|
-
|
|
323
|
-
// Disabled
|
|
324
|
-
const disabledClasses = option.disabled ? 'opacity-50 cursor-not-allowed' : '';
|
|
325
|
-
|
|
326
|
-
return [baseMenuItemClasses, colorClasses, hoverClasses, disabledClasses]
|
|
327
|
-
.join(' ')
|
|
328
|
-
.replace(/\s+/g, ' ')
|
|
329
|
-
.trim();
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
// ===== CLASES DEL ICONO DEL TRIGGER =====
|
|
333
|
-
const iconClasses = [
|
|
334
|
-
'flex-shrink-0',
|
|
335
|
-
'transition-transform',
|
|
336
|
-
'duration-150',
|
|
337
|
-
isOpen && 'rotate-180',
|
|
338
|
-
disabled ? 'text-content-tertiary dark:text-content-tertiary' : 'text-content-secondary dark:text-content-secondary',
|
|
339
|
-
]
|
|
340
|
-
.filter(Boolean)
|
|
341
|
-
.join(' ')
|
|
342
|
-
.replace(/\s+/g, ' ')
|
|
343
|
-
.trim();
|
|
344
|
-
|
|
345
|
-
// ===== CLASES DEL CHECK ICON =====
|
|
346
|
-
const checkIconClasses = 'flex-shrink-0 w-4 h-4';
|
|
347
|
-
|
|
348
|
-
// ===== RENDERIZAR COMPONENTE =====
|
|
349
|
-
return (
|
|
350
|
-
<div className={`relative ${fullWidth ? 'w-full' : ''} ${className}`} ref={containerRef}>
|
|
351
|
-
{/* Label and Description - Según Figma, van ANTES del trigger con gap-1 (4px) */}
|
|
352
|
-
{(showLabel && label) || (showDescription && description) ? (
|
|
353
|
-
<div className="flex flex-col gap-1 mb-1 w-full">
|
|
354
|
-
{/* Label - Label Small (14px bold) */}
|
|
355
|
-
{showLabel && label && (
|
|
356
|
-
<label
|
|
357
|
-
htmlFor={id}
|
|
358
|
-
className="text-sm font-bold leading-5 text-content-primary dark:text-dark-content-primary"
|
|
359
|
-
>
|
|
360
|
-
{label}
|
|
361
|
-
{required && <span className="ml-1 text-error-content">*</span>}
|
|
362
|
-
</label>
|
|
363
|
-
)}
|
|
364
|
-
|
|
365
|
-
{/* Description - Paragraph Small (14px regular) */}
|
|
366
|
-
{showDescription && description && (
|
|
367
|
-
<p className="text-sm font-normal leading-5 text-content-tertiary dark:text-content-tertiary">
|
|
368
|
-
{description}
|
|
369
|
-
</p>
|
|
370
|
-
)}
|
|
371
|
-
</div>
|
|
372
|
-
) : null}
|
|
373
|
-
|
|
374
|
-
{/* Trigger Button */}
|
|
375
|
-
<button
|
|
376
|
-
ref={triggerRef}
|
|
377
|
-
type="button"
|
|
378
|
-
className={finalTriggerClasses}
|
|
379
|
-
onClick={toggleMenu}
|
|
380
|
-
disabled={disabled}
|
|
381
|
-
aria-label={ariaLabel || label}
|
|
382
|
-
aria-haspopup="listbox"
|
|
383
|
-
aria-expanded={isOpen}
|
|
384
|
-
id={id}
|
|
385
|
-
>
|
|
386
|
-
{/* Selected value or placeholder */}
|
|
387
|
-
<span
|
|
388
|
-
className={
|
|
389
|
-
selectedOption ? '' : 'text-content-secondary dark:text-content-secondary'
|
|
390
|
-
}
|
|
391
|
-
>
|
|
392
|
-
{selectedOption ? selectedOption.label : placeholder}
|
|
393
|
-
</span>
|
|
394
|
-
|
|
395
|
-
{/* Chevron icon */}
|
|
396
|
-
<ChevronUpDownIcon className={iconClasses} />
|
|
397
|
-
</button>
|
|
398
|
-
|
|
399
|
-
{/* Hidden input for forms */}
|
|
400
|
-
{name && (
|
|
401
|
-
<input
|
|
402
|
-
type="hidden"
|
|
403
|
-
name={name}
|
|
404
|
-
value={selectedValue !== undefined ? String(selectedValue) : ''}
|
|
405
|
-
/>
|
|
406
|
-
)}
|
|
407
|
-
|
|
408
|
-
{/* Menu */}
|
|
409
|
-
{isOpen && (
|
|
410
|
-
<div ref={menuRef} className={finalMenuClasses} role="listbox">
|
|
411
|
-
{/* Menu Header - Opcional según Figma */}
|
|
412
|
-
{menuHeader && (
|
|
413
|
-
<div className="flex items-center gap-0.5 px-6 py-0.5 rounded-[5px]">
|
|
414
|
-
<span className="text-[10px] font-bold leading-[12px] text-content-tertiary dark:text-content-tertiary">
|
|
415
|
-
{menuHeader}
|
|
416
|
-
</span>
|
|
417
|
-
</div>
|
|
418
|
-
)}
|
|
419
|
-
|
|
420
|
-
{/* Empty state */}
|
|
421
|
-
{options.length === 0 && (
|
|
422
|
-
<div className="px-2 py-1.5 text-[10px] leading-[12px] text-content-secondary dark:text-content-secondary">
|
|
423
|
-
No hay opciones disponibles
|
|
424
|
-
</div>
|
|
425
|
-
)}
|
|
426
|
-
|
|
427
|
-
{/* Options */}
|
|
428
|
-
{options.map((option, index) => (
|
|
429
|
-
<div
|
|
430
|
-
key={option.value}
|
|
431
|
-
className={getMenuItemClasses(option, index)}
|
|
432
|
-
onClick={() => handleSelect(option)}
|
|
433
|
-
onMouseEnter={() => setFocusedIndex(index)}
|
|
434
|
-
role="option"
|
|
435
|
-
aria-selected={option.value === selectedValue}
|
|
436
|
-
aria-disabled={option.disabled}
|
|
437
|
-
>
|
|
438
|
-
{/* Check icon for selected item */}
|
|
439
|
-
<div className="w-4 flex items-center justify-center">
|
|
440
|
-
{option.value === selectedValue && <CheckIcon className={checkIconClasses} />}
|
|
441
|
-
</div>
|
|
442
|
-
|
|
443
|
-
{/* Custom icon if provided */}
|
|
444
|
-
{option.icon && <span className="flex-shrink-0 w-4 h-4">{option.icon}</span>}
|
|
445
|
-
|
|
446
|
-
{/* Label */}
|
|
447
|
-
<span className="flex-1 text-left">{option.label}</span>
|
|
448
|
-
</div>
|
|
449
|
-
))}
|
|
450
|
-
</div>
|
|
451
|
-
)}
|
|
452
|
-
</div>
|
|
453
|
-
);
|
|
454
|
-
};
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from 'react';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Opción individual del Select
|
|
5
|
-
*/
|
|
6
|
-
export interface SelectOption {
|
|
7
|
-
/**
|
|
8
|
-
* Valor único de la opción
|
|
9
|
-
*/
|
|
10
|
-
value: string | number;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Label visible de la opción
|
|
14
|
-
*/
|
|
15
|
-
label: string;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Si la opción está deshabilitada
|
|
19
|
-
* @default false
|
|
20
|
-
*/
|
|
21
|
-
disabled?: boolean;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Icono personalizado para la opción
|
|
25
|
-
*/
|
|
26
|
-
icon?: ReactNode;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Props del componente Select
|
|
31
|
-
*/
|
|
32
|
-
export interface SelectProps {
|
|
33
|
-
/**
|
|
34
|
-
* Opciones disponibles para seleccionar
|
|
35
|
-
*/
|
|
36
|
-
options: SelectOption[];
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Valor seleccionado actual
|
|
40
|
-
*/
|
|
41
|
-
value?: string | number;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Valor por defecto inicial
|
|
45
|
-
*/
|
|
46
|
-
defaultValue?: string | number;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Texto del placeholder cuando no hay selección
|
|
50
|
-
* @default 'Seleccionar...'
|
|
51
|
-
*/
|
|
52
|
-
placeholder?: string;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Si el select está deshabilitado
|
|
56
|
-
* @default false
|
|
57
|
-
*/
|
|
58
|
-
disabled?: boolean;
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Si el select muestra un error
|
|
62
|
-
* @default false
|
|
63
|
-
*/
|
|
64
|
-
error?: boolean;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Label del select (opcional)
|
|
68
|
-
*/
|
|
69
|
-
label?: string;
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Texto descriptivo que aparece debajo del label y antes del trigger
|
|
73
|
-
* Equivale al "Description" de Figma
|
|
74
|
-
* @example "This will be visible to clients on the project."
|
|
75
|
-
*/
|
|
76
|
-
description?: string;
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Muestra el label
|
|
80
|
-
* @default true
|
|
81
|
-
*/
|
|
82
|
-
showLabel?: boolean;
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Muestra la descripción
|
|
86
|
-
* @default true
|
|
87
|
-
*/
|
|
88
|
-
showDescription?: boolean;
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Header opcional que aparece al inicio del menú
|
|
92
|
-
* @example "Header ..."
|
|
93
|
-
*/
|
|
94
|
-
menuHeader?: string;
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Handler cuando cambia la selección
|
|
98
|
-
*/
|
|
99
|
-
onChange?: (value: string | number) => void;
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Clases CSS adicionales
|
|
103
|
-
*/
|
|
104
|
-
className?: string;
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Clases CSS adicionales para el trigger
|
|
108
|
-
*/
|
|
109
|
-
triggerClassName?: string;
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Clases CSS adicionales para el menu
|
|
113
|
-
*/
|
|
114
|
-
menuClassName?: string;
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Label para accesibilidad (ARIA)
|
|
118
|
-
*/
|
|
119
|
-
ariaLabel?: string;
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* ID del select para asociar con label
|
|
123
|
-
*/
|
|
124
|
-
id?: string;
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Nombre del campo para formularios
|
|
128
|
-
*/
|
|
129
|
-
name?: string;
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Si el campo es requerido
|
|
133
|
-
* @default false
|
|
134
|
-
*/
|
|
135
|
-
required?: boolean;
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Posición del menú desplegable
|
|
139
|
-
* @default 'bottom'
|
|
140
|
-
*/
|
|
141
|
-
menuPosition?: 'top' | 'bottom';
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Ancho completo
|
|
145
|
-
* @default false
|
|
146
|
-
*/
|
|
147
|
-
fullWidth?: boolean;
|
|
148
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Icono ChevronUpDown para el trigger del Select
|
|
5
|
-
* Heroicons Micro (16x16px)
|
|
6
|
-
*/
|
|
7
|
-
export const ChevronUpDownIcon: React.FC<{ className?: string }> = ({ className = '' }) => {
|
|
8
|
-
return (
|
|
9
|
-
<svg
|
|
10
|
-
className={className}
|
|
11
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
12
|
-
viewBox="0 0 16 16"
|
|
13
|
-
fill="currentColor"
|
|
14
|
-
aria-hidden="true"
|
|
15
|
-
>
|
|
16
|
-
<path
|
|
17
|
-
fillRule="evenodd"
|
|
18
|
-
d="M11.78 9.78a.75.75 0 0 1-1.06 0L8 7.06 5.28 9.78a.75.75 0 0 1-1.06-1.06l3.25-3.25a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06Z"
|
|
19
|
-
clipRule="evenodd"
|
|
20
|
-
/>
|
|
21
|
-
<path
|
|
22
|
-
fillRule="evenodd"
|
|
23
|
-
d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z"
|
|
24
|
-
clipRule="evenodd"
|
|
25
|
-
/>
|
|
26
|
-
</svg>
|
|
27
|
-
);
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Icono Check para los items seleccionados del menu
|
|
32
|
-
* Heroicons Micro (16x16px)
|
|
33
|
-
*/
|
|
34
|
-
export const CheckIcon: React.FC<{ className?: string }> = ({ className = '' }) => {
|
|
35
|
-
return (
|
|
36
|
-
<svg
|
|
37
|
-
className={className}
|
|
38
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
39
|
-
viewBox="0 0 16 16"
|
|
40
|
-
fill="currentColor"
|
|
41
|
-
aria-hidden="true"
|
|
42
|
-
>
|
|
43
|
-
<path
|
|
44
|
-
fillRule="evenodd"
|
|
45
|
-
d="M12.416 3.376a.75.75 0 0 1 .208 1.04l-5 7.5a.75.75 0 0 1-1.154.114l-3-3a.75.75 0 0 1 1.06-1.06l2.353 2.353 4.493-6.74a.75.75 0 0 1 1.04-.207Z"
|
|
46
|
-
clipRule="evenodd"
|
|
47
|
-
/>
|
|
48
|
-
</svg>
|
|
49
|
-
);
|
|
50
|
-
};
|