siesa-ui-kit 1.0.2 → 1.0.4
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 +115 -115
- package/bin/install.cjs +502 -502
- package/bin/prepare-publish.cjs +28 -28
- package/bin/restore-folders.cjs +28 -28
- package/claude/agents/siesa-ui-kit-specialist.md +2445 -0
- package/claude/prompts/component-template.md +121 -0
- package/claude/prompts/siesa-ui-kit.md +28 -0
- package/claude/settings.local.json +67 -2
- package/dist/components/Button/icons.d.ts +6 -5
- package/dist/components/Button/icons.d.ts.map +1 -1
- package/dist/components/DropdownItemCollapsible/DropdownItemCollapsible.d.ts.map +1 -1
- package/dist/components/DropdownItemCollapsible/DropdownItemCollapsible.types.d.ts +21 -0
- package/dist/components/DropdownItemCollapsible/DropdownItemCollapsible.types.d.ts.map +1 -1
- package/dist/components/NavigationRailCommercial/NavigationRailCommercial.d.ts +122 -0
- package/dist/components/NavigationRailCommercial/NavigationRailCommercial.d.ts.map +1 -0
- package/dist/components/NavigationRailCommercial/NavigationRailCommercial.types.d.ts +139 -0
- package/dist/components/NavigationRailCommercial/NavigationRailCommercial.types.d.ts.map +1 -0
- package/dist/components/NavigationRailCommercial/icons.d.ts +33 -0
- package/dist/components/NavigationRailCommercial/icons.d.ts.map +1 -0
- package/dist/components/NavigationRailCommercial/index.d.ts +4 -0
- package/dist/components/NavigationRailCommercial/index.d.ts.map +1 -0
- package/dist/components/NavigationRailItem/NavigationRailItem.d.ts.map +1 -1
- package/dist/components/NavigationRailItem/NavigationRailItem.types.d.ts +7 -0
- package/dist/components/NavigationRailItem/NavigationRailItem.types.d.ts.map +1 -1
- package/dist/components/NavigationRailTypes/NavigationRailTypes.d.ts.map +1 -1
- package/dist/components/NavigationRailTypes/NavigationRailTypes.types.d.ts +41 -0
- package/dist/components/NavigationRailTypes/NavigationRailTypes.types.d.ts.map +1 -1
- package/dist/components/NavigationRailTypes/icons.d.ts +15 -29
- package/dist/components/NavigationRailTypes/icons.d.ts.map +1 -1
- package/dist/components/Select/Select.d.ts.map +1 -1
- package/dist/components/Select/icons.d.ts +6 -2
- package/dist/components/Select/icons.d.ts.map +1 -1
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/siesa-ui-kit.cjs +404 -190
- package/dist/siesa-ui-kit.cjs.map +1 -1
- package/dist/siesa-ui-kit.mjs +6590 -1506
- package/dist/siesa-ui-kit.mjs.map +1 -1
- package/dist/views/LayoutCommercial/LayoutCommercial.d.ts +48 -0
- package/dist/views/LayoutCommercial/LayoutCommercial.d.ts.map +1 -0
- package/dist/views/LayoutCommercial/LayoutCommercial.types.d.ts +49 -0
- package/dist/views/LayoutCommercial/LayoutCommercial.types.d.ts.map +1 -0
- package/dist/views/LayoutCommercial/index.d.ts +3 -0
- package/dist/views/LayoutCommercial/index.d.ts.map +1 -0
- package/docs/icons.md +12 -31
- package/package.json +111 -110
- package/src/components/Avatar/Avatar.stories.tsx +494 -494
- package/src/components/Button/Button.stories.tsx +950 -950
- package/src/components/Button/Button.tsx +337 -337
- package/src/components/Button/Button.types.ts +180 -180
- package/src/components/Button/icons.tsx +23 -62
- package/src/components/DescriptionList/DescriptionList.stories.tsx +250 -250
- package/src/components/Divider/Divider.stories.tsx +263 -263
- package/src/components/DropdownItemCollapsible/DropdownItemCollapsible.stories.tsx +317 -317
- package/src/components/DropdownItemCollapsible/DropdownItemCollapsible.tsx +307 -287
- package/src/components/DropdownItemCollapsible/DropdownItemCollapsible.types.ts +136 -111
- package/src/components/DropdownItemCollapsible/README.md +264 -264
- package/src/components/DropdownItemCollapsible/icons.tsx +57 -57
- package/src/components/DropdownItemCollapsible/index.ts +12 -12
- package/src/components/DropdownItemHeading/DropdownItemHeading.stories.tsx +386 -386
- package/src/components/DropdownItemHeading/DropdownItemHeading.tsx +216 -216
- package/src/components/DropdownItemHeading/DropdownItemHeading.types.ts +93 -93
- package/src/components/DropdownItemHeading/README.md +573 -573
- package/src/components/DropdownItemHeading/icons.tsx +125 -125
- package/src/components/DropdownItemHeading/index.ts +3 -3
- package/src/components/Input/Input.stories.tsx +583 -583
- package/src/components/LoginView/LoginView.stories.tsx +148 -148
- package/src/components/LoginView/LoginView.tsx +426 -426
- package/src/components/LoginView/LoginView.types.ts +52 -52
- package/src/components/LoginView/README.md +396 -396
- package/src/components/LoginView/icons.tsx +85 -85
- package/src/components/LoginView/index.ts +3 -3
- package/src/components/Navbar/Navbar.stories.tsx +810 -810
- package/src/components/Navbar/Navbar.tsx +755 -755
- package/src/components/Navbar/Navbar.types.ts +219 -219
- package/src/components/Navbar/README.md +279 -279
- package/src/components/Navbar/index.ts +8 -8
- package/src/components/NavigationRailCommercial/NavigationRailCommercial.stories.tsx +464 -0
- package/src/components/NavigationRailCommercial/NavigationRailCommercial.tsx +301 -0
- package/src/components/NavigationRailCommercial/NavigationRailCommercial.types.ts +162 -0
- package/src/components/NavigationRailCommercial/README.md +251 -0
- package/src/components/NavigationRailCommercial/icons.tsx +54 -0
- package/src/components/NavigationRailCommercial/index.ts +6 -0
- package/src/components/NavigationRailItem/NavigationRailItem.stories.tsx +667 -667
- package/src/components/NavigationRailItem/NavigationRailItem.tsx +314 -313
- package/src/components/NavigationRailItem/NavigationRailItem.types.ts +175 -167
- package/src/components/NavigationRailItem/README.md +476 -476
- package/src/components/NavigationRailItem/index.ts +2 -2
- package/src/components/NavigationRailPanel/NavigationRailPanel.stories.tsx +462 -462
- package/src/components/NavigationRailPanel/NavigationRailPanel.tsx +332 -332
- package/src/components/NavigationRailPanel/NavigationRailPanel.types.ts +178 -178
- package/src/components/NavigationRailPanel/README.md +461 -461
- package/src/components/NavigationRailPanel/index.ts +6 -6
- package/src/components/NavigationRailTypes/NavigationRailTypes.stories.tsx +682 -528
- package/src/components/NavigationRailTypes/NavigationRailTypes.tsx +363 -378
- package/src/components/NavigationRailTypes/NavigationRailTypes.types.ts +178 -130
- package/src/components/NavigationRailTypes/README.md +573 -573
- package/src/components/NavigationRailTypes/icons.tsx +76 -141
- package/src/components/NavigationRailTypes/index.ts +7 -7
- package/src/components/Notification/Notification.stories.tsx +513 -513
- package/src/components/Notification/Notification.tsx +145 -145
- package/src/components/Notification/Notification.types.ts +142 -142
- package/src/components/Notification/README.md +409 -409
- package/src/components/POSConvention/POSConvention.stories.tsx +235 -235
- package/src/components/POSConvention/POSConvention.tsx +129 -129
- package/src/components/POSConvention/POSConvention.types.ts +38 -38
- package/src/components/POSConvention/README.md +123 -123
- package/src/components/POSConvention/icons.tsx +45 -45
- package/src/components/POSConvention/index.ts +3 -3
- package/src/components/POSLocationButton/POSLocationButton.stories.tsx +531 -531
- package/src/components/POSLocationButton/POSLocationButton.tsx +247 -247
- package/src/components/POSLocationButton/POSLocationButton.types.ts +87 -87
- package/src/components/POSLocationButton/README.md +253 -253
- package/src/components/POSLocationButton/icons.tsx +120 -120
- package/src/components/POSLocationButton/index.ts +14 -14
- package/src/components/POSNumberButton/POSNumberButton.stories.tsx +415 -415
- package/src/components/POSNumberButton/POSNumberButton.tsx +179 -179
- package/src/components/POSNumberButton/POSNumberButton.types.ts +51 -51
- package/src/components/POSNumberButton/README.md +321 -321
- package/src/components/POSNumberButton/index.ts +3 -3
- package/src/components/POSProductButton/POSProductButton.stories.tsx +318 -318
- package/src/components/POSProductCard/POSProductCard.stories.tsx +642 -642
- package/src/components/POSProductCard/POSProductCard.tsx +208 -208
- package/src/components/POSProductCard/POSProductCard.types.ts +76 -76
- package/src/components/POSProductCard/README.md +179 -179
- package/src/components/POSProductCard/icons.tsx +26 -26
- package/src/components/POSProductCard/index.ts +2 -2
- package/src/components/POSProductSidebarItems/POSProductSidebarItems.stories.tsx +753 -753
- package/src/components/POSProductSidebarItems/POSProductSidebarItems.tsx +332 -332
- package/src/components/POSProductSidebarItems/POSProductSidebarItems.types.ts +119 -119
- package/src/components/POSProductSidebarItems/README.md +198 -198
- package/src/components/POSProductSidebarItems/icons.tsx +21 -21
- package/src/components/POSProductSidebarItems/index.ts +3 -3
- package/src/components/POSTable/POSTable.stories.tsx +737 -737
- package/src/components/POSTable/POSTable.tsx +401 -401
- package/src/components/POSTable/README.md +286 -286
- package/src/components/Quantity/Quantity.stories.tsx +457 -457
- package/src/components/Radio/Radio.stories.tsx +523 -523
- package/src/components/Radio/Radio.tsx +1 -1
- package/src/components/Select/Select.stories.tsx +32 -0
- package/src/components/Select/Select.tsx +457 -454
- package/src/components/Select/icons.tsx +16 -41
- package/src/components/SignUpView/SignUpView.stories.tsx +129 -129
- package/src/components/SignUpView/SignUpView.tsx +503 -503
- package/src/components/SignUpView/SignUpView.types.ts +58 -58
- package/src/components/SignUpView/icons.tsx +71 -71
- package/src/components/SignUpView/index.ts +3 -3
- package/src/components/Switch/README.md +112 -112
- package/src/components/Switch/Switch.stories.tsx +550 -550
- package/src/components/Switch/Switch.tsx +246 -246
- package/src/components/Switch/Switch.types.ts +67 -67
- package/src/components/Table/Table.stories.tsx +805 -805
- package/src/components/Tabs/README.md +201 -201
- package/src/components/Tabs/Tabs.stories.tsx +580 -580
- package/src/components/Tabs/Tabs.tsx +356 -356
- package/src/components/Tabs/Tabs.types.ts +127 -127
- package/src/components/Tabs/icons.tsx +129 -129
- package/src/components/Tabs/index.ts +11 -11
- package/src/components/Textarea/Textarea.stories.tsx +535 -535
- package/src/index.ts +133 -102
- package/src/views/LayoutCommercial/LayoutCommercial.stories.tsx +374 -0
- package/src/views/LayoutCommercial/LayoutCommercial.tsx +125 -0
- package/src/views/LayoutCommercial/LayoutCommercial.types.ts +54 -0
- package/src/views/LayoutCommercial/README.md +286 -0
- package/src/views/LayoutCommercial/index.ts +2 -0
- package/src/views/ListView/ListView.stories.tsx +329 -329
- package/src/views/ListView/ListView.tsx +570 -570
- package/src/views/ListView/ListView.types.ts +211 -211
- package/src/views/ListView/icons.tsx +282 -282
- package/src/views/ListView/index.ts +11 -11
- package/src/views/LoginView/LoginView.tsx +426 -426
- package/src/views/ProductsView/ProductsView.stories.tsx +344 -344
- package/src/views/ProductsView/ProductsView.tsx +480 -480
- package/src/views/ProductsView/ProductsView.types.ts +238 -238
- package/src/views/ProductsView/README.md +312 -312
- package/src/views/ProductsView/icons.tsx +38 -38
- package/src/views/ProductsView/index.ts +8 -8
- package/src/views/RecoverPasswordView/RecoverPasswordView.tsx +376 -376
- package/src/views/SignUpView/SignUpView.tsx +503 -503
- package/src/views/TableLayoutView/README.md +268 -268
- package/src/views/TableLayoutView/TableLayoutView.stories.tsx +235 -235
- package/src/views/TableLayoutView/TableLayoutView.tsx +461 -461
- package/src/views/TableLayoutView/TableLayoutView.types.ts +209 -209
- package/src/views/TableLayoutView/icons.tsx +113 -113
- package/src/views/TableLayoutView/index.ts +6 -6
- package/storybook/main.ts +19 -19
- package/storybook/preview.tsx +84 -84
- package/storybook/vitest.setup.ts +6 -6
- package/tailwind.config.js +128 -128
|
@@ -1,356 +1,356 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import type { TabProps, TabsProps } from './Tabs.types';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Componente Tab individual del sistema de diseño Siesa
|
|
6
|
-
*
|
|
7
|
-
* Representa un único tab dentro de un grupo de tabs. Incluye soporte para:
|
|
8
|
-
* - Estado activo/inactivo con indicador visual (línea inferior)
|
|
9
|
-
* - Icono opcional a la izquierda del texto
|
|
10
|
-
* - Badge de notificación con contador
|
|
11
|
-
* - Estados hover, focus y disabled
|
|
12
|
-
* - Dark mode completo
|
|
13
|
-
*
|
|
14
|
-
* Mejores prácticas implementadas:
|
|
15
|
-
* - Orden de modificadores: {responsive}:{dark}:{state}:{utility}
|
|
16
|
-
* - Dark mode con estrategia 'class' (darkMode: 'class')
|
|
17
|
-
* - Tokens de color consistentes con la documentación
|
|
18
|
-
* - Type safety con TypeScript estricto
|
|
19
|
-
* - Accesibilidad con ARIA labels
|
|
20
|
-
*
|
|
21
|
-
* @see docs/colors.md - Sistema de colores
|
|
22
|
-
* @see docs/typography.md - Sistema tipográfico
|
|
23
|
-
* @see docs/spacing.md - Sistema de espaciado
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* ```tsx
|
|
27
|
-
* <Tab
|
|
28
|
-
* label="Inicio"
|
|
29
|
-
* active={true}
|
|
30
|
-
* icon={<HomeIcon />}
|
|
31
|
-
* badge={3}
|
|
32
|
-
* onClick={() => setActiveTab('home')}
|
|
33
|
-
* />
|
|
34
|
-
* ```
|
|
35
|
-
*/
|
|
36
|
-
export const Tab: React.FC<TabProps> = ({
|
|
37
|
-
label,
|
|
38
|
-
active = false,
|
|
39
|
-
icon,
|
|
40
|
-
badge,
|
|
41
|
-
disabled = false,
|
|
42
|
-
onClick,
|
|
43
|
-
className = '',
|
|
44
|
-
ariaLabel,
|
|
45
|
-
}) => {
|
|
46
|
-
// ===== CLASES BASE DEL BOTÓN =====
|
|
47
|
-
// El contenedor es relativo para posicionar el indicador absoluto
|
|
48
|
-
const baseClasses = `
|
|
49
|
-
relative
|
|
50
|
-
flex
|
|
51
|
-
flex-col
|
|
52
|
-
items-center
|
|
53
|
-
cursor-pointer
|
|
54
|
-
outline-none
|
|
55
|
-
transition-all
|
|
56
|
-
duration-150
|
|
57
|
-
p-0
|
|
58
|
-
border-0
|
|
59
|
-
bg-transparent
|
|
60
|
-
`;
|
|
61
|
-
|
|
62
|
-
// ===== CLASES DE FOCUS =====
|
|
63
|
-
// El focus outline se desactiva aquí, el shadow se aplica al Content div
|
|
64
|
-
const focusClasses = '';
|
|
65
|
-
|
|
66
|
-
// ===== CLASES DE DISABLED =====
|
|
67
|
-
const disabledClasses = disabled
|
|
68
|
-
? 'opacity-50 cursor-not-allowed pointer-events-none'
|
|
69
|
-
: '';
|
|
70
|
-
|
|
71
|
-
// ===== CLASES DEL CONTENIDO INTERIOR =====
|
|
72
|
-
// El hover y focus background se aplica aquí
|
|
73
|
-
const contentBaseClasses = `
|
|
74
|
-
flex
|
|
75
|
-
items-center
|
|
76
|
-
justify-center
|
|
77
|
-
gap-1
|
|
78
|
-
p-2
|
|
79
|
-
rounded-lg
|
|
80
|
-
overflow-hidden
|
|
81
|
-
transition-all
|
|
82
|
-
duration-150
|
|
83
|
-
w-full
|
|
84
|
-
`;
|
|
85
|
-
|
|
86
|
-
// ===== CLASES DE ESTADO HOVER Y FOCUS PARA CONTENIDO =====
|
|
87
|
-
// Light: bg-[rgba(0,0,0,0.03)], Dark: bg-white/5
|
|
88
|
-
// Según Figma, tanto hover como focus tienen el background overlay
|
|
89
|
-
// El shadow de focus también se aplica aquí (al Content div), no al botón
|
|
90
|
-
const contentStateClasses = disabled
|
|
91
|
-
? ''
|
|
92
|
-
: `
|
|
93
|
-
group-hover:bg-[rgba(0,0,0,0.03)]
|
|
94
|
-
group-focus-visible:bg-[rgba(0,0,0,0.03)]
|
|
95
|
-
group-focus-visible:shadow-
|
|
96
|
-
dark:group-hover:bg-white/5
|
|
97
|
-
dark:group-focus-visible:bg-white/5
|
|
98
|
-
dark:group-focus-visible:shadow-
|
|
99
|
-
`;
|
|
100
|
-
|
|
101
|
-
// ===== CLASES DE TEXTO =====
|
|
102
|
-
// Activo: primary-custom-600 (#0e79fd), Inactivo: content-primary (#18181b)
|
|
103
|
-
const textClasses = active
|
|
104
|
-
? `
|
|
105
|
-
text-primary-custom-600
|
|
106
|
-
dark:text-primary-custom-600
|
|
107
|
-
`
|
|
108
|
-
: `
|
|
109
|
-
text-content-primary
|
|
110
|
-
dark:text-dark-content-primary
|
|
111
|
-
`;
|
|
112
|
-
|
|
113
|
-
// ===== CLASES DE ICONO =====
|
|
114
|
-
// El icono hereda el color del texto
|
|
115
|
-
const iconClasses = `
|
|
116
|
-
w-3
|
|
117
|
-
h-3
|
|
118
|
-
flex-shrink-0
|
|
119
|
-
`;
|
|
120
|
-
|
|
121
|
-
// ===== CLASES DEL BADGE =====
|
|
122
|
-
// Activo: bg-primary-custom-600 (#0e79fd), Inactivo: bg-content-primary (#18181b)
|
|
123
|
-
const badgeClasses = active
|
|
124
|
-
? `
|
|
125
|
-
bg-primary-custom-600
|
|
126
|
-
dark:bg-primary-custom-600
|
|
127
|
-
`
|
|
128
|
-
: `
|
|
129
|
-
bg-content-primary
|
|
130
|
-
dark:bg-dark-content-primary
|
|
131
|
-
`;
|
|
132
|
-
|
|
133
|
-
// ===== CLASES DEL INDICADOR INFERIOR =====
|
|
134
|
-
// Solo visible cuando está activo, 2px de alto, rounded-full
|
|
135
|
-
// Posicionado al fondo del contenedor de Tabs (alineado con el border)
|
|
136
|
-
const indicatorClasses = active
|
|
137
|
-
? `
|
|
138
|
-
absolute
|
|
139
|
-
-bottom-2.5
|
|
140
|
-
left-0
|
|
141
|
-
right-0
|
|
142
|
-
h-0.5
|
|
143
|
-
rounded-full
|
|
144
|
-
bg-primary-custom-600
|
|
145
|
-
dark:bg-primary-custom-600
|
|
146
|
-
z-10
|
|
147
|
-
`
|
|
148
|
-
: 'hidden';
|
|
149
|
-
|
|
150
|
-
// ===== COMBINAR CLASES DEL BOTÓN =====
|
|
151
|
-
const finalClasses = [
|
|
152
|
-
baseClasses,
|
|
153
|
-
focusClasses,
|
|
154
|
-
disabledClasses,
|
|
155
|
-
'group', // Para usar group-hover y group-focus-visible
|
|
156
|
-
className,
|
|
157
|
-
]
|
|
158
|
-
.join(' ')
|
|
159
|
-
.replace(/\s+/g, ' ')
|
|
160
|
-
.trim();
|
|
161
|
-
|
|
162
|
-
// ===== COMBINAR CLASES DEL CONTENIDO =====
|
|
163
|
-
const contentFinalClasses = [
|
|
164
|
-
contentBaseClasses,
|
|
165
|
-
contentStateClasses,
|
|
166
|
-
]
|
|
167
|
-
.join(' ')
|
|
168
|
-
.replace(/\s+/g, ' ')
|
|
169
|
-
.trim();
|
|
170
|
-
|
|
171
|
-
return (
|
|
172
|
-
<button
|
|
173
|
-
type="button"
|
|
174
|
-
role="tab"
|
|
175
|
-
aria-selected={active}
|
|
176
|
-
aria-disabled={disabled}
|
|
177
|
-
aria-label={ariaLabel || label}
|
|
178
|
-
tabIndex={disabled ? -1 : 0}
|
|
179
|
-
className={finalClasses}
|
|
180
|
-
onClick={disabled ? undefined : onClick}
|
|
181
|
-
disabled={disabled}
|
|
182
|
-
>
|
|
183
|
-
{/* Contenido del Tab */}
|
|
184
|
-
<div className={contentFinalClasses}>
|
|
185
|
-
{/* Icono (opcional) */}
|
|
186
|
-
{icon && (
|
|
187
|
-
<span className={`${iconClasses} ${textClasses}`.trim()}>
|
|
188
|
-
{icon}
|
|
189
|
-
</span>
|
|
190
|
-
)}
|
|
191
|
-
|
|
192
|
-
{/* Texto */}
|
|
193
|
-
<span
|
|
194
|
-
className={`
|
|
195
|
-
text-sm
|
|
196
|
-
font-bold
|
|
197
|
-
leading-5
|
|
198
|
-
whitespace-nowrap
|
|
199
|
-
${textClasses}
|
|
200
|
-
`
|
|
201
|
-
.replace(/\s+/g, ' ')
|
|
202
|
-
.trim()}
|
|
203
|
-
>
|
|
204
|
-
{label}
|
|
205
|
-
</span>
|
|
206
|
-
|
|
207
|
-
{/* Badge de notificación (opcional) */}
|
|
208
|
-
{badge !== undefined && badge > 0 && (
|
|
209
|
-
<span
|
|
210
|
-
className={`
|
|
211
|
-
flex
|
|
212
|
-
items-center
|
|
213
|
-
justify-center
|
|
214
|
-
h-3
|
|
215
|
-
min-w-[12px]
|
|
216
|
-
px-0.5
|
|
217
|
-
rounded-sm
|
|
218
|
-
text-xs
|
|
219
|
-
font-normal
|
|
220
|
-
leading-4
|
|
221
|
-
text-primary-inverse-content
|
|
222
|
-
dark:text-dark-bg-primary
|
|
223
|
-
${badgeClasses}
|
|
224
|
-
`
|
|
225
|
-
.replace(/\s+/g, ' ')
|
|
226
|
-
.trim()}
|
|
227
|
-
aria-label={`${badge} notificaciones`}
|
|
228
|
-
>
|
|
229
|
-
{badge > 99 ? '99+' : badge}
|
|
230
|
-
</span>
|
|
231
|
-
)}
|
|
232
|
-
</div>
|
|
233
|
-
|
|
234
|
-
{/* Indicador inferior (línea activa) */}
|
|
235
|
-
<div className={indicatorClasses} />
|
|
236
|
-
</button>
|
|
237
|
-
);
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Componente Tabs contenedor del sistema de diseño Siesa
|
|
242
|
-
*
|
|
243
|
-
* Agrupa múltiples tabs permitiendo navegación entre diferentes secciones.
|
|
244
|
-
* Soporta modo controlado (con activeId y onChange) y no controlado (con defaultActiveId).
|
|
245
|
-
*
|
|
246
|
-
* Mejores prácticas implementadas:
|
|
247
|
-
* - Orden de modificadores: {responsive}:{dark}:{state}:{utility}
|
|
248
|
-
* - Dark mode con estrategia 'class' (darkMode: 'class')
|
|
249
|
-
* - Tokens de color consistentes con la documentación
|
|
250
|
-
* - Type safety con TypeScript estricto
|
|
251
|
-
* - Accesibilidad con ARIA roles y keyboard navigation
|
|
252
|
-
*
|
|
253
|
-
* @see docs/colors.md - Sistema de colores
|
|
254
|
-
* @see docs/typography.md - Sistema tipográfico
|
|
255
|
-
* @see docs/spacing.md - Sistema de espaciado
|
|
256
|
-
*
|
|
257
|
-
* @example
|
|
258
|
-
* ```tsx
|
|
259
|
-
* // Modo controlado
|
|
260
|
-
* const [activeTab, setActiveTab] = useState('home');
|
|
261
|
-
*
|
|
262
|
-
* <Tabs
|
|
263
|
-
* items={[
|
|
264
|
-
* { id: 'home', label: 'Inicio', icon: <HomeIcon /> },
|
|
265
|
-
* { id: 'profile', label: 'Perfil', badge: 3 },
|
|
266
|
-
* { id: 'settings', label: 'Configuración' },
|
|
267
|
-
* ]}
|
|
268
|
-
* activeId={activeTab}
|
|
269
|
-
* onChange={setActiveTab}
|
|
270
|
-
* />
|
|
271
|
-
*
|
|
272
|
-
* // Modo no controlado
|
|
273
|
-
* <Tabs
|
|
274
|
-
* items={[...]}
|
|
275
|
-
* defaultActiveId="home"
|
|
276
|
-
* />
|
|
277
|
-
* ```
|
|
278
|
-
*/
|
|
279
|
-
export const Tabs: React.FC<TabsProps> = ({
|
|
280
|
-
items,
|
|
281
|
-
activeId,
|
|
282
|
-
defaultActiveId,
|
|
283
|
-
onChange,
|
|
284
|
-
className = '',
|
|
285
|
-
fullWidth = false,
|
|
286
|
-
size = 'base',
|
|
287
|
-
showBorder = true,
|
|
288
|
-
}) => {
|
|
289
|
-
// Estado interno para modo no controlado
|
|
290
|
-
const [internalActiveId, setInternalActiveId] = useState(
|
|
291
|
-
defaultActiveId || (items.length > 0 ? items[0].id : '')
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
// Determinar si es controlado o no controlado
|
|
295
|
-
const isControlled = activeId !== undefined;
|
|
296
|
-
const currentActiveId = isControlled ? activeId : internalActiveId;
|
|
297
|
-
|
|
298
|
-
// Handler para cambio de tab
|
|
299
|
-
const handleTabClick = (id: string) => {
|
|
300
|
-
if (!isControlled) {
|
|
301
|
-
setInternalActiveId(id);
|
|
302
|
-
}
|
|
303
|
-
onChange?.(id);
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
// ===== CLASES DE TAMAÑO =====
|
|
307
|
-
const sizeClasses = {
|
|
308
|
-
sm: 'gap-0',
|
|
309
|
-
base: 'gap-1',
|
|
310
|
-
lg: 'gap-2',
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
// ===== CLASES DEL CONTENEDOR =====
|
|
314
|
-
// El padding-bottom (pb-2.5 = 10px) deja espacio para el indicador del tab activo
|
|
315
|
-
// El border-b se dibuja debajo del padding, alineándose con el indicador
|
|
316
|
-
const containerClasses = `
|
|
317
|
-
flex
|
|
318
|
-
items-end
|
|
319
|
-
pb-2.5
|
|
320
|
-
${sizeClasses[size]}
|
|
321
|
-
${fullWidth ? 'w-full' : ''}
|
|
322
|
-
${showBorder ? 'border-b border-border-primary dark:border-dark-border-primary' : ''}
|
|
323
|
-
`;
|
|
324
|
-
|
|
325
|
-
// ===== CLASES FULL WIDTH PARA ITEMS =====
|
|
326
|
-
const itemWidthClass = fullWidth ? 'flex-1' : '';
|
|
327
|
-
|
|
328
|
-
const finalClasses = [
|
|
329
|
-
containerClasses,
|
|
330
|
-
className,
|
|
331
|
-
]
|
|
332
|
-
.join(' ')
|
|
333
|
-
.replace(/\s+/g, ' ')
|
|
334
|
-
.trim();
|
|
335
|
-
|
|
336
|
-
return (
|
|
337
|
-
<div
|
|
338
|
-
role="tablist"
|
|
339
|
-
aria-label="Pestañas de navegación"
|
|
340
|
-
className={finalClasses}
|
|
341
|
-
>
|
|
342
|
-
{items.map((item) => (
|
|
343
|
-
<Tab
|
|
344
|
-
key={item.id}
|
|
345
|
-
label={item.label}
|
|
346
|
-
active={currentActiveId === item.id}
|
|
347
|
-
icon={item.icon}
|
|
348
|
-
badge={item.badge}
|
|
349
|
-
disabled={item.disabled}
|
|
350
|
-
onClick={() => handleTabClick(item.id)}
|
|
351
|
-
className={itemWidthClass}
|
|
352
|
-
/>
|
|
353
|
-
))}
|
|
354
|
-
</div>
|
|
355
|
-
);
|
|
356
|
-
};
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import type { TabProps, TabsProps } from './Tabs.types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Componente Tab individual del sistema de diseño Siesa
|
|
6
|
+
*
|
|
7
|
+
* Representa un único tab dentro de un grupo de tabs. Incluye soporte para:
|
|
8
|
+
* - Estado activo/inactivo con indicador visual (línea inferior)
|
|
9
|
+
* - Icono opcional a la izquierda del texto
|
|
10
|
+
* - Badge de notificación con contador
|
|
11
|
+
* - Estados hover, focus y disabled
|
|
12
|
+
* - Dark mode completo
|
|
13
|
+
*
|
|
14
|
+
* Mejores prácticas implementadas:
|
|
15
|
+
* - Orden de modificadores: {responsive}:{dark}:{state}:{utility}
|
|
16
|
+
* - Dark mode con estrategia 'class' (darkMode: 'class')
|
|
17
|
+
* - Tokens de color consistentes con la documentación
|
|
18
|
+
* - Type safety con TypeScript estricto
|
|
19
|
+
* - Accesibilidad con ARIA labels
|
|
20
|
+
*
|
|
21
|
+
* @see docs/colors.md - Sistema de colores
|
|
22
|
+
* @see docs/typography.md - Sistema tipográfico
|
|
23
|
+
* @see docs/spacing.md - Sistema de espaciado
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* <Tab
|
|
28
|
+
* label="Inicio"
|
|
29
|
+
* active={true}
|
|
30
|
+
* icon={<HomeIcon />}
|
|
31
|
+
* badge={3}
|
|
32
|
+
* onClick={() => setActiveTab('home')}
|
|
33
|
+
* />
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export const Tab: React.FC<TabProps> = ({
|
|
37
|
+
label,
|
|
38
|
+
active = false,
|
|
39
|
+
icon,
|
|
40
|
+
badge,
|
|
41
|
+
disabled = false,
|
|
42
|
+
onClick,
|
|
43
|
+
className = '',
|
|
44
|
+
ariaLabel,
|
|
45
|
+
}) => {
|
|
46
|
+
// ===== CLASES BASE DEL BOTÓN =====
|
|
47
|
+
// El contenedor es relativo para posicionar el indicador absoluto
|
|
48
|
+
const baseClasses = `
|
|
49
|
+
relative
|
|
50
|
+
flex
|
|
51
|
+
flex-col
|
|
52
|
+
items-center
|
|
53
|
+
cursor-pointer
|
|
54
|
+
outline-none
|
|
55
|
+
transition-all
|
|
56
|
+
duration-150
|
|
57
|
+
p-0
|
|
58
|
+
border-0
|
|
59
|
+
bg-transparent
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
// ===== CLASES DE FOCUS =====
|
|
63
|
+
// El focus outline se desactiva aquí, el shadow se aplica al Content div
|
|
64
|
+
const focusClasses = '';
|
|
65
|
+
|
|
66
|
+
// ===== CLASES DE DISABLED =====
|
|
67
|
+
const disabledClasses = disabled
|
|
68
|
+
? 'opacity-50 cursor-not-allowed pointer-events-none'
|
|
69
|
+
: '';
|
|
70
|
+
|
|
71
|
+
// ===== CLASES DEL CONTENIDO INTERIOR =====
|
|
72
|
+
// El hover y focus background se aplica aquí
|
|
73
|
+
const contentBaseClasses = `
|
|
74
|
+
flex
|
|
75
|
+
items-center
|
|
76
|
+
justify-center
|
|
77
|
+
gap-1
|
|
78
|
+
p-2
|
|
79
|
+
rounded-lg
|
|
80
|
+
overflow-hidden
|
|
81
|
+
transition-all
|
|
82
|
+
duration-150
|
|
83
|
+
w-full
|
|
84
|
+
`;
|
|
85
|
+
|
|
86
|
+
// ===== CLASES DE ESTADO HOVER Y FOCUS PARA CONTENIDO =====
|
|
87
|
+
// Light: bg-[rgba(0,0,0,0.03)], Dark: bg-white/5
|
|
88
|
+
// Según Figma, tanto hover como focus tienen el background overlay
|
|
89
|
+
// El shadow de focus también se aplica aquí (al Content div), no al botón
|
|
90
|
+
const contentStateClasses = disabled
|
|
91
|
+
? ''
|
|
92
|
+
: `
|
|
93
|
+
group-hover:bg-[rgba(0,0,0,0.03)]
|
|
94
|
+
group-focus-visible:bg-[rgba(0,0,0,0.03)]
|
|
95
|
+
group-focus-visible:shadow-lg
|
|
96
|
+
dark:group-hover:bg-white/5
|
|
97
|
+
dark:group-focus-visible:bg-white/5
|
|
98
|
+
dark:group-focus-visible:shadow-2xl
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
// ===== CLASES DE TEXTO =====
|
|
102
|
+
// Activo: primary-custom-600 (#0e79fd), Inactivo: content-primary (#18181b)
|
|
103
|
+
const textClasses = active
|
|
104
|
+
? `
|
|
105
|
+
text-primary-custom-600
|
|
106
|
+
dark:text-primary-custom-600
|
|
107
|
+
`
|
|
108
|
+
: `
|
|
109
|
+
text-content-primary
|
|
110
|
+
dark:text-dark-content-primary
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
// ===== CLASES DE ICONO =====
|
|
114
|
+
// El icono hereda el color del texto
|
|
115
|
+
const iconClasses = `
|
|
116
|
+
w-3
|
|
117
|
+
h-3
|
|
118
|
+
flex-shrink-0
|
|
119
|
+
`;
|
|
120
|
+
|
|
121
|
+
// ===== CLASES DEL BADGE =====
|
|
122
|
+
// Activo: bg-primary-custom-600 (#0e79fd), Inactivo: bg-content-primary (#18181b)
|
|
123
|
+
const badgeClasses = active
|
|
124
|
+
? `
|
|
125
|
+
bg-primary-custom-600
|
|
126
|
+
dark:bg-primary-custom-600
|
|
127
|
+
`
|
|
128
|
+
: `
|
|
129
|
+
bg-content-primary
|
|
130
|
+
dark:bg-dark-content-primary
|
|
131
|
+
`;
|
|
132
|
+
|
|
133
|
+
// ===== CLASES DEL INDICADOR INFERIOR =====
|
|
134
|
+
// Solo visible cuando está activo, 2px de alto, rounded-full
|
|
135
|
+
// Posicionado al fondo del contenedor de Tabs (alineado con el border)
|
|
136
|
+
const indicatorClasses = active
|
|
137
|
+
? `
|
|
138
|
+
absolute
|
|
139
|
+
-bottom-2.5
|
|
140
|
+
left-0
|
|
141
|
+
right-0
|
|
142
|
+
h-0.5
|
|
143
|
+
rounded-full
|
|
144
|
+
bg-primary-custom-600
|
|
145
|
+
dark:bg-primary-custom-600
|
|
146
|
+
z-10
|
|
147
|
+
`
|
|
148
|
+
: 'hidden';
|
|
149
|
+
|
|
150
|
+
// ===== COMBINAR CLASES DEL BOTÓN =====
|
|
151
|
+
const finalClasses = [
|
|
152
|
+
baseClasses,
|
|
153
|
+
focusClasses,
|
|
154
|
+
disabledClasses,
|
|
155
|
+
'group', // Para usar group-hover y group-focus-visible
|
|
156
|
+
className,
|
|
157
|
+
]
|
|
158
|
+
.join(' ')
|
|
159
|
+
.replace(/\s+/g, ' ')
|
|
160
|
+
.trim();
|
|
161
|
+
|
|
162
|
+
// ===== COMBINAR CLASES DEL CONTENIDO =====
|
|
163
|
+
const contentFinalClasses = [
|
|
164
|
+
contentBaseClasses,
|
|
165
|
+
contentStateClasses,
|
|
166
|
+
]
|
|
167
|
+
.join(' ')
|
|
168
|
+
.replace(/\s+/g, ' ')
|
|
169
|
+
.trim();
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<button
|
|
173
|
+
type="button"
|
|
174
|
+
role="tab"
|
|
175
|
+
aria-selected={active}
|
|
176
|
+
aria-disabled={disabled}
|
|
177
|
+
aria-label={ariaLabel || label}
|
|
178
|
+
tabIndex={disabled ? -1 : 0}
|
|
179
|
+
className={finalClasses}
|
|
180
|
+
onClick={disabled ? undefined : onClick}
|
|
181
|
+
disabled={disabled}
|
|
182
|
+
>
|
|
183
|
+
{/* Contenido del Tab */}
|
|
184
|
+
<div className={contentFinalClasses}>
|
|
185
|
+
{/* Icono (opcional) */}
|
|
186
|
+
{icon && (
|
|
187
|
+
<span className={`${iconClasses} ${textClasses}`.trim()}>
|
|
188
|
+
{icon}
|
|
189
|
+
</span>
|
|
190
|
+
)}
|
|
191
|
+
|
|
192
|
+
{/* Texto */}
|
|
193
|
+
<span
|
|
194
|
+
className={`
|
|
195
|
+
text-sm
|
|
196
|
+
font-bold
|
|
197
|
+
leading-5
|
|
198
|
+
whitespace-nowrap
|
|
199
|
+
${textClasses}
|
|
200
|
+
`
|
|
201
|
+
.replace(/\s+/g, ' ')
|
|
202
|
+
.trim()}
|
|
203
|
+
>
|
|
204
|
+
{label}
|
|
205
|
+
</span>
|
|
206
|
+
|
|
207
|
+
{/* Badge de notificación (opcional) */}
|
|
208
|
+
{badge !== undefined && badge > 0 && (
|
|
209
|
+
<span
|
|
210
|
+
className={`
|
|
211
|
+
flex
|
|
212
|
+
items-center
|
|
213
|
+
justify-center
|
|
214
|
+
h-3
|
|
215
|
+
min-w-[12px]
|
|
216
|
+
px-0.5
|
|
217
|
+
rounded-sm
|
|
218
|
+
text-xs
|
|
219
|
+
font-normal
|
|
220
|
+
leading-4
|
|
221
|
+
text-primary-inverse-content
|
|
222
|
+
dark:text-dark-bg-primary
|
|
223
|
+
${badgeClasses}
|
|
224
|
+
`
|
|
225
|
+
.replace(/\s+/g, ' ')
|
|
226
|
+
.trim()}
|
|
227
|
+
aria-label={`${badge} notificaciones`}
|
|
228
|
+
>
|
|
229
|
+
{badge > 99 ? '99+' : badge}
|
|
230
|
+
</span>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
{/* Indicador inferior (línea activa) */}
|
|
235
|
+
<div className={indicatorClasses} />
|
|
236
|
+
</button>
|
|
237
|
+
);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Componente Tabs contenedor del sistema de diseño Siesa
|
|
242
|
+
*
|
|
243
|
+
* Agrupa múltiples tabs permitiendo navegación entre diferentes secciones.
|
|
244
|
+
* Soporta modo controlado (con activeId y onChange) y no controlado (con defaultActiveId).
|
|
245
|
+
*
|
|
246
|
+
* Mejores prácticas implementadas:
|
|
247
|
+
* - Orden de modificadores: {responsive}:{dark}:{state}:{utility}
|
|
248
|
+
* - Dark mode con estrategia 'class' (darkMode: 'class')
|
|
249
|
+
* - Tokens de color consistentes con la documentación
|
|
250
|
+
* - Type safety con TypeScript estricto
|
|
251
|
+
* - Accesibilidad con ARIA roles y keyboard navigation
|
|
252
|
+
*
|
|
253
|
+
* @see docs/colors.md - Sistema de colores
|
|
254
|
+
* @see docs/typography.md - Sistema tipográfico
|
|
255
|
+
* @see docs/spacing.md - Sistema de espaciado
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* ```tsx
|
|
259
|
+
* // Modo controlado
|
|
260
|
+
* const [activeTab, setActiveTab] = useState('home');
|
|
261
|
+
*
|
|
262
|
+
* <Tabs
|
|
263
|
+
* items={[
|
|
264
|
+
* { id: 'home', label: 'Inicio', icon: <HomeIcon /> },
|
|
265
|
+
* { id: 'profile', label: 'Perfil', badge: 3 },
|
|
266
|
+
* { id: 'settings', label: 'Configuración' },
|
|
267
|
+
* ]}
|
|
268
|
+
* activeId={activeTab}
|
|
269
|
+
* onChange={setActiveTab}
|
|
270
|
+
* />
|
|
271
|
+
*
|
|
272
|
+
* // Modo no controlado
|
|
273
|
+
* <Tabs
|
|
274
|
+
* items={[...]}
|
|
275
|
+
* defaultActiveId="home"
|
|
276
|
+
* />
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
export const Tabs: React.FC<TabsProps> = ({
|
|
280
|
+
items,
|
|
281
|
+
activeId,
|
|
282
|
+
defaultActiveId,
|
|
283
|
+
onChange,
|
|
284
|
+
className = '',
|
|
285
|
+
fullWidth = false,
|
|
286
|
+
size = 'base',
|
|
287
|
+
showBorder = true,
|
|
288
|
+
}) => {
|
|
289
|
+
// Estado interno para modo no controlado
|
|
290
|
+
const [internalActiveId, setInternalActiveId] = useState(
|
|
291
|
+
defaultActiveId || (items.length > 0 ? items[0].id : '')
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
// Determinar si es controlado o no controlado
|
|
295
|
+
const isControlled = activeId !== undefined;
|
|
296
|
+
const currentActiveId = isControlled ? activeId : internalActiveId;
|
|
297
|
+
|
|
298
|
+
// Handler para cambio de tab
|
|
299
|
+
const handleTabClick = (id: string) => {
|
|
300
|
+
if (!isControlled) {
|
|
301
|
+
setInternalActiveId(id);
|
|
302
|
+
}
|
|
303
|
+
onChange?.(id);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// ===== CLASES DE TAMAÑO =====
|
|
307
|
+
const sizeClasses = {
|
|
308
|
+
sm: 'gap-0',
|
|
309
|
+
base: 'gap-1',
|
|
310
|
+
lg: 'gap-2',
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// ===== CLASES DEL CONTENEDOR =====
|
|
314
|
+
// El padding-bottom (pb-2.5 = 10px) deja espacio para el indicador del tab activo
|
|
315
|
+
// El border-b se dibuja debajo del padding, alineándose con el indicador
|
|
316
|
+
const containerClasses = `
|
|
317
|
+
flex
|
|
318
|
+
items-end
|
|
319
|
+
pb-2.5
|
|
320
|
+
${sizeClasses[size]}
|
|
321
|
+
${fullWidth ? 'w-full' : ''}
|
|
322
|
+
${showBorder ? 'border-b border-border-primary dark:border-dark-border-primary' : ''}
|
|
323
|
+
`;
|
|
324
|
+
|
|
325
|
+
// ===== CLASES FULL WIDTH PARA ITEMS =====
|
|
326
|
+
const itemWidthClass = fullWidth ? 'flex-1' : '';
|
|
327
|
+
|
|
328
|
+
const finalClasses = [
|
|
329
|
+
containerClasses,
|
|
330
|
+
className,
|
|
331
|
+
]
|
|
332
|
+
.join(' ')
|
|
333
|
+
.replace(/\s+/g, ' ')
|
|
334
|
+
.trim();
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<div
|
|
338
|
+
role="tablist"
|
|
339
|
+
aria-label="Pestañas de navegación"
|
|
340
|
+
className={finalClasses}
|
|
341
|
+
>
|
|
342
|
+
{items.map((item) => (
|
|
343
|
+
<Tab
|
|
344
|
+
key={item.id}
|
|
345
|
+
label={item.label}
|
|
346
|
+
active={currentActiveId === item.id}
|
|
347
|
+
icon={item.icon}
|
|
348
|
+
badge={item.badge}
|
|
349
|
+
disabled={item.disabled}
|
|
350
|
+
onClick={() => handleTabClick(item.id)}
|
|
351
|
+
className={itemWidthClass}
|
|
352
|
+
/>
|
|
353
|
+
))}
|
|
354
|
+
</div>
|
|
355
|
+
);
|
|
356
|
+
};
|