anima-ds-nucleus 1.0.16 → 1.0.17

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": "anima-ds-nucleus",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
4
4
  "description": "Anima Design System - A comprehensive React component library",
5
5
  "author": "Nucleus Labs <ipvasallo@nucleus.com.ar>",
6
6
  "license": "UNLICENSED",
@@ -0,0 +1,192 @@
1
+ import { useState } from 'react';
2
+ import { Icon } from '../../Atoms/Icon/Icon';
3
+ import { Avatar } from '../../Atoms/Avatar/Avatar';
4
+ import { Typography } from '../../Atoms/Typography/Typography';
5
+
6
+ // Constantes de diseño reutilizables
7
+ const NOTIFICATION_BADGE_COLOR = '#6D3856';
8
+
9
+ export const HeaderCompartido = ({
10
+ searchPlaceholder = 'Buscar empleados, reportes, configuraciones...',
11
+ userName,
12
+ userAvatar,
13
+ notificationCount = 0,
14
+ onSearch,
15
+ onNotificationClick,
16
+ onUserClick,
17
+ // Borde inferior (subrayado)
18
+ showBottomBorder = false,
19
+ bottomBorderClassName = 'border-b border-gray-200',
20
+ // Overrides de estilo/clases para poder ajustar layout desde el proyecto consumidor
21
+ desktopLayoutClassName = '',
22
+ desktopLayoutStyle,
23
+ desktopSearchContainerClassName = '',
24
+ desktopSearchContainerStyle,
25
+ className = '',
26
+ ...props
27
+ }) => {
28
+ const [searchValue, setSearchValue] = useState('');
29
+
30
+ const formatNotificationCount = (count) => {
31
+ return count > 9 ? '9+' : count;
32
+ };
33
+
34
+ const handleSearchChange = (e) => {
35
+ const value = e.target.value;
36
+ setSearchValue(value);
37
+ if (onSearch) {
38
+ onSearch(value);
39
+ }
40
+ };
41
+
42
+ const handleSearchSubmit = (e) => {
43
+ e.preventDefault();
44
+ if (onSearch) {
45
+ onSearch(searchValue);
46
+ }
47
+ };
48
+
49
+ return (
50
+ <header
51
+ className={`bg-white ${showBottomBorder ? bottomBorderClassName : ''} ${className}`}
52
+ {...props}
53
+ >
54
+ <style>{`
55
+ .header-search-input {
56
+ border-color: #9ca3af;
57
+ }
58
+ .header-search-input::placeholder {
59
+ color: #9ca3af !important;
60
+ opacity: 1 !important;
61
+ }
62
+ .header-search-icon-container {
63
+ border-color: #9ca3af;
64
+ }
65
+ .header-search-form:focus-within .header-search-input {
66
+ border-color: #3b82f6 !important;
67
+ box-shadow: 0 0 0 1px #3b82f6;
68
+ }
69
+ .header-search-form:focus-within .header-search-icon-container {
70
+ border-color: #3b82f6 !important;
71
+ box-shadow: 0 0 0 1px #3b82f6;
72
+ }
73
+ .header-notification-badge {
74
+ background-color: ${NOTIFICATION_BADGE_COLOR};
75
+ border-radius: 12px;
76
+ }
77
+ .header-actions-wrapper {
78
+ flex-shrink: 0;
79
+ }
80
+ `}</style>
81
+
82
+ <div className="w-full" style={{ paddingLeft: '4px', paddingRight: '4px' }}>
83
+ {/* Layout Desktop */}
84
+ <div
85
+ className={`header-desktop-layout flex items-center justify-between h-16 ${desktopLayoutClassName}`}
86
+ style={desktopLayoutStyle}
87
+ >
88
+ {/* Barra de búsqueda con icono a la derecha */}
89
+ <div
90
+ className={`flex-1 mr-2 md:mr-4 ${desktopSearchContainerClassName}`}
91
+ style={{ maxWidth: 'none', minWidth: '0', ...(desktopSearchContainerStyle || {}) }}
92
+ >
93
+ <form onSubmit={handleSearchSubmit} className="header-search-form flex items-center w-full">
94
+ {/* Input de búsqueda */}
95
+ <input
96
+ type="text"
97
+ value={searchValue}
98
+ onChange={handleSearchChange}
99
+ placeholder={searchPlaceholder}
100
+ className="header-search-input flex-1 pl-2 md:pl-4 pr-2 md:pr-4 py-2 md:py-2.5 bg-white border border-gray-400 rounded-l-lg
101
+ focus:outline-none text-sm md:text-base"
102
+ style={{
103
+ height: '40px',
104
+ borderRight: 'none',
105
+ borderTopRightRadius: '0',
106
+ borderBottomRightRadius: '0',
107
+ color: '#374151',
108
+ fontFamily: 'IBM Plex Sans',
109
+ fontWeight: 400,
110
+ fontStyle: 'italic',
111
+ fontSize: '14px',
112
+ lineHeight: '20px',
113
+ letterSpacing: '0px'
114
+ }}
115
+ />
116
+
117
+ {/* Recuadro con icono de buscar a la derecha */}
118
+ <div
119
+ className="header-search-icon-container flex items-center justify-center border border-gray-400 rounded-r-lg bg-white"
120
+ style={{
121
+ width: '40px',
122
+ height: '40px',
123
+ borderTopLeftRadius: '0',
124
+ borderBottomLeftRadius: '0'
125
+ }}
126
+ >
127
+ <Icon
128
+ name="MagnifyingGlassIcon"
129
+ variant="24-outline"
130
+ size={20}
131
+ className="color-gray-600"
132
+ />
133
+ </div>
134
+ </form>
135
+ </div>
136
+
137
+ {/* Notificaciones y Perfil */}
138
+ <div className="header-actions-wrapper flex items-center gap-3 md:gap-6">
139
+ {notificationCount !== undefined && (
140
+ <button
141
+ onClick={onNotificationClick}
142
+ className="relative p-2 hover:bg-gray-100 rounded-lg transition-colors"
143
+ aria-label="Notificaciones"
144
+ >
145
+ <Icon
146
+ name="BellIcon"
147
+ variant="24-outline"
148
+ size={24}
149
+ className="color-gray-600"
150
+ />
151
+ {notificationCount > 0 && (
152
+ <span className="absolute -top-1 -right-1 px-1.5 py-0.5 min-w-[20px] h-5 text-white rounded-full flex items-center justify-center text-body-sm font-medium header-notification-badge">
153
+ {formatNotificationCount(notificationCount)}
154
+ </span>
155
+ )}
156
+ </button>
157
+ )}
158
+
159
+ {userName && (
160
+ <button
161
+ onClick={onUserClick}
162
+ className="flex items-center gap-3 hover:bg-gray-50 rounded-lg px-2 py-1.5 transition-colors"
163
+ aria-label="Perfil de usuario"
164
+ >
165
+ <Avatar
166
+ src={userAvatar}
167
+ name={userName}
168
+ size="medium"
169
+ variant="circle"
170
+ />
171
+ <Typography
172
+ variant="body-lg"
173
+ className="color-gray-700 hidden md:block header-user-name"
174
+ >
175
+ {userName}
176
+ </Typography>
177
+ <Icon
178
+ name="ChevronDownIcon"
179
+ variant="24-outline"
180
+ size={20}
181
+ className="color-gray-500 header-user-chevron hidden md:block"
182
+ />
183
+ </button>
184
+ )}
185
+ </div>
186
+ </div>
187
+ </div>
188
+ </header>
189
+ );
190
+ };
191
+
192
+ export default HeaderCompartido;
@@ -0,0 +1,66 @@
1
+ import { HeaderCompartido } from './HeaderCompartido';
2
+
3
+ export default {
4
+ title: 'Layout/HeaderCompartido',
5
+ component: HeaderCompartido,
6
+ tags: ['autodocs'],
7
+ };
8
+
9
+ export const Default = {
10
+ render: () => (
11
+ <HeaderCompartido
12
+ searchPlaceholder="Buscar empleados, reportes, configuraciones..."
13
+ userName="Maria García Alonso"
14
+ userAvatar="https://i.pravatar.cc/150?img=12"
15
+ notificationCount={7}
16
+ onSearch={(value) => console.log('Búsqueda:', value)}
17
+ onNotificationClick={() => console.log('Notificaciones clickeadas')}
18
+ onUserClick={() => console.log('Usuario clickeado')}
19
+ />
20
+ ),
21
+ };
22
+
23
+ export const SinNotificaciones = {
24
+ render: () => (
25
+ <HeaderCompartido
26
+ searchPlaceholder="Buscar empleados, reportes, configuraciones..."
27
+ userName="Juan Pérez"
28
+ userAvatar="https://i.pravatar.cc/150?img=33"
29
+ notificationCount={0}
30
+ />
31
+ ),
32
+ };
33
+
34
+ export const ConNotificaciones = {
35
+ render: () => (
36
+ <HeaderCompartido
37
+ searchPlaceholder="Buscar empleados, reportes, configuraciones..."
38
+ userName="Maria García Alonso"
39
+ userAvatar="https://i.pravatar.cc/150?img=12"
40
+ notificationCount={7}
41
+ onSearch={(value) => console.log('Búsqueda:', value)}
42
+ onNotificationClick={() => console.log('Notificaciones clickeadas')}
43
+ onUserClick={() => console.log('Usuario clickeado')}
44
+ />
45
+ ),
46
+ };
47
+
48
+ export const ConMuchasNotificaciones = {
49
+ render: () => (
50
+ <HeaderCompartido
51
+ searchPlaceholder="Buscar empleados, reportes, configuraciones..."
52
+ userName="Ana Martínez"
53
+ userAvatar="https://i.pravatar.cc/150?img=47"
54
+ notificationCount={15}
55
+ />
56
+ ),
57
+ };
58
+
59
+ export const SinUsuario = {
60
+ render: () => (
61
+ <HeaderCompartido
62
+ searchPlaceholder="Buscar empleados, reportes, configuraciones..."
63
+ notificationCount={3}
64
+ />
65
+ ),
66
+ };
@@ -4,6 +4,7 @@ import { Typography } from '../../Atoms/Typography/Typography';
4
4
  import { LogoHexa } from '../../Atoms/LogoHexa/LogoHexa';
5
5
 
6
6
  export const HeaderGeneral = ({
7
+ variant,
7
8
  logoText = 'HEXA',
8
9
  suiteText = 'Suite',
9
10
  languages = [
@@ -16,6 +17,9 @@ export const HeaderGeneral = ({
16
17
  ...props
17
18
  }) => {
18
19
  const [isLanguageOpen, setIsLanguageOpen] = useState(false);
20
+ const [hoveredLangCode, setHoveredLangCode] = useState(null);
21
+ const [isLanguageButtonHovered, setIsLanguageButtonHovered] = useState(false);
22
+ const isLoginVariant = variant === 'login';
19
23
 
20
24
  const currentLanguage = languages.find(lang => lang.code === currentLanguageCode) || languages[0];
21
25
 
@@ -27,64 +31,229 @@ export const HeaderGeneral = ({
27
31
  };
28
32
 
29
33
  return (
30
- <header className={`w-full flex justify-between items-center bg-white ${className}`} style={{ paddingTop: '4px', paddingBottom: '4px', paddingLeft: '8px', paddingRight: '12px' }} {...props}>
31
- {/* Logo */}
32
- <div className="flex items-center gap-2">
33
- <LogoHexa width={32} height={36} />
34
- <div className="flex items-center" style={{ gap: '4px' }}>
35
- <Typography variant="h6" className="font-normal" style={{ color: '#2D5C63', margin: 0 }}>
36
- {logoText}
37
- </Typography>
38
- <Typography variant="h6" className="font-normal" style={{ color: '#2D5C63', margin: 0 }}>
39
- {suiteText}
40
- </Typography>
41
- </div>
42
- </div>
34
+ isLoginVariant ? (() => {
35
+ const { style: styleProp, ...restProps } = props;
43
36
 
44
- {/* Language Selector - Solo se muestra si hay idiomas */}
45
- {languages && languages.length > 0 && (
46
- <div className="relative flex items-center gap-2">
47
- <Icon name="LanguageIcon" variant="24-outline" size={18} style={{ color: '#374151' }} />
48
- <button
49
- onClick={() => setIsLanguageOpen(!isLanguageOpen)}
50
- className="flex items-center gap-2 px-3 py-2 text-body-md color-gray-700 bg-white hover:bg-gray-50 rounded-lg transition-colors"
51
- style={{ border: '1px solid #2D5C63' }}
52
- >
53
- <span>{currentLanguage.name}</span>
54
- <Icon
55
- name="ChevronDownIcon"
56
- variant="24-outline"
57
- size={16}
58
- className={`transition-transform ${isLanguageOpen ? 'rotate-180' : ''}`}
59
- />
60
- </button>
37
+ return (
38
+ <header
39
+ className={`w-full flex justify-between items-center bg-white ${className}`}
40
+ style={{
41
+ boxShadow: '0px 2px 4px -1px #0000000F, 0px 4px 6px -1px #0000001A',
42
+ padding: '16px var(--mf-ds-grid-margin, 20px)',
43
+ ...styleProp,
44
+ }}
45
+ {...restProps}
46
+ >
47
+ {/* Logo */}
48
+ <div className="flex items-center gap-3">
49
+ <LogoHexa width={36} height={40} />
50
+ <div className="flex items-center" style={{ gap: '4px' }}>
51
+ <Typography variant="h5" className="font-normal" style={{ color: '#2D5C63', margin: 0 }}>
52
+ {logoText}
53
+ </Typography>
54
+ <Typography variant="h5" className="font-normal" style={{ color: '#2D5C63', margin: 0 }}>
55
+ {suiteText}
56
+ </Typography>
57
+ </div>
58
+ </div>
61
59
 
62
- {isLanguageOpen && (
63
- <>
64
- <div
65
- className="fixed inset-0 z-10"
66
- onClick={() => setIsLanguageOpen(false)}
67
- />
68
- <div className="absolute right-0 mt-2 w-40 bg-white rounded-lg shadow-lg z-20 py-1" style={{ border: '1px solid #2D5C63' }}>
69
- {languages.map((lang) => (
70
- <button
71
- key={lang.code}
72
- onClick={() => handleLanguageChange(lang.code)}
73
- className={`w-full text-left px-4 py-2 text-body-md hover:bg-gray-50 transition-colors ${
74
- currentLanguage.code === lang.code
75
- ? 'bg-gray-50 color-brand font-medium'
76
- : 'color-gray-700'
77
- }`}
78
- >
79
- {lang.name}
80
- </button>
81
- ))}
60
+ {/* Language Selector - Solo se muestra si hay idiomas */}
61
+ {languages && languages.length > 0 && (
62
+ <div className="relative">
63
+ <div className="flex items-center" style={{ width: '150px', height: '40px', gap: '16px', opacity: 1 }}>
64
+ <Icon
65
+ name="LanguageIcon"
66
+ variant="24-outline"
67
+ size={20}
68
+ style={{ width: '20px', height: '20px', opacity: 1 }}
69
+ />
70
+ <button
71
+ onClick={() => setIsLanguageOpen(!isLanguageOpen)}
72
+ onMouseEnter={() => setIsLanguageButtonHovered(true)}
73
+ onMouseLeave={() => setIsLanguageButtonHovered(false)}
74
+ className="flex items-center gap-2 text-body-md text-gray-700 bg-white hover:bg-gray-50 transition-colors"
75
+ style={{
76
+ width: '114px',
77
+ height: '40px',
78
+ padding: '8px 12px 8px 16px',
79
+ gap: '8px',
80
+ borderRadius: '8px',
81
+ border: '1px solid #38656D',
82
+ background: isLanguageButtonHovered ? '#F9FAFB' : 'white',
83
+ opacity: 1,
84
+ color: '#374151',
85
+ }}
86
+ >
87
+ <span>{currentLanguage.name}</span>
88
+ <Icon
89
+ name="ChevronDownIcon"
90
+ variant="24-outline"
91
+ size={16}
92
+ className={`transition-transform ${isLanguageOpen ? 'rotate-180' : ''}`}
93
+ />
94
+ </button>
82
95
  </div>
83
- </>
96
+
97
+ {isLanguageOpen && (
98
+ <>
99
+ <div
100
+ className="fixed inset-0 z-10"
101
+ onClick={() => setIsLanguageOpen(false)}
102
+ />
103
+ <div
104
+ className="absolute right-0 mt-2 bg-white z-20"
105
+ style={{
106
+ width: '224px',
107
+ height: '112px',
108
+ padding: '12px',
109
+ borderRadius: '12px',
110
+ border: '1px solid #E5E7EB',
111
+ boxShadow: '0px 2px 4px -2px #0000001A, 0px 4px 6px -1px #0000001A',
112
+ display: 'flex',
113
+ flexDirection: 'column',
114
+ gap: '8px',
115
+ }}
116
+ >
117
+ {languages.map((lang) => (
118
+ <button
119
+ key={lang.code}
120
+ onClick={() => handleLanguageChange(lang.code)}
121
+ className="w-full text-left transition-colors"
122
+ style={{
123
+ width: '200px',
124
+ height: '40px',
125
+ padding: 0,
126
+ border: 'none',
127
+ background: 'transparent',
128
+ }}
129
+ onMouseEnter={() => setHoveredLangCode(lang.code)}
130
+ onMouseLeave={() => setHoveredLangCode(null)}
131
+ >
132
+ <div
133
+ style={{
134
+ width: '200px',
135
+ height: '40px',
136
+ padding: '8px',
137
+ borderRadius: '8px',
138
+ display: 'flex',
139
+ alignItems: 'center',
140
+ justifyContent: 'space-between',
141
+ gap: '16px',
142
+ backgroundColor:
143
+ currentLanguage.code === lang.code
144
+ ? '#6D3856'
145
+ : hoveredLangCode === lang.code
146
+ ? '#DDDDDD'
147
+ : 'transparent',
148
+ color: currentLanguage.code === lang.code ? '#FFFFFF' : undefined,
149
+ }}
150
+ >
151
+ <span
152
+ style={{
153
+ fontFamily: 'IBM Plex Sans',
154
+ fontWeight: 400,
155
+ fontStyle: 'normal',
156
+ fontSize: '16px',
157
+ lineHeight: '24px',
158
+ letterSpacing: '0px',
159
+ width: '144px',
160
+ height: '24px',
161
+ opacity: 1,
162
+ }}
163
+ >
164
+ {lang.name}
165
+ </span>
166
+
167
+ {currentLanguage.code === lang.code && (
168
+ <div
169
+ style={{
170
+ width: '24px',
171
+ height: '24px',
172
+ backgroundColor: '#FFFFFF',
173
+ borderRadius: '999px',
174
+ display: 'flex',
175
+ alignItems: 'center',
176
+ justifyContent: 'center',
177
+ }}
178
+ >
179
+ <Icon
180
+ name="CheckCircleIcon"
181
+ variant="24-solid"
182
+ size={24}
183
+ style={{ width: '24px', height: '24px', color: '#6D3856' }}
184
+ />
185
+ </div>
186
+ )}
187
+ </div>
188
+ </button>
189
+ ))}
190
+ </div>
191
+ </>
192
+ )}
193
+ </div>
84
194
  )}
195
+ </header>
196
+ );
197
+ })() : (
198
+ <header className={`w-full flex justify-between items-center bg-white ${className}`} style={{ paddingTop: '4px', paddingBottom: '4px', paddingLeft: '8px', paddingRight: '12px' }} {...props}>
199
+ {/* Logo */}
200
+ <div className="flex items-center gap-2">
201
+ <LogoHexa width={32} height={36} />
202
+ <div className="flex items-center" style={{ gap: '4px' }}>
203
+ <Typography variant="h6" className="font-normal" style={{ color: '#2D5C63', margin: 0 }}>
204
+ {logoText}
205
+ </Typography>
206
+ <Typography variant="h6" className="font-normal" style={{ color: '#2D5C63', margin: 0 }}>
207
+ {suiteText}
208
+ </Typography>
209
+ </div>
85
210
  </div>
86
- )}
87
- </header>
211
+
212
+ {/* Language Selector - Solo se muestra si hay idiomas */}
213
+ {languages && languages.length > 0 && (
214
+ <div className="relative flex items-center gap-2">
215
+ <Icon name="LanguageIcon" variant="24-outline" size={18} style={{ color: '#374151' }} />
216
+ <button
217
+ onClick={() => setIsLanguageOpen(!isLanguageOpen)}
218
+ className="flex items-center gap-2 px-3 py-2 text-body-md color-gray-700 bg-white hover:bg-gray-50 rounded-lg transition-colors"
219
+ style={{ border: '1px solid #2D5C63' }}
220
+ >
221
+ <span>{currentLanguage.name}</span>
222
+ <Icon
223
+ name="ChevronDownIcon"
224
+ variant="24-outline"
225
+ size={16}
226
+ className={`transition-transform ${isLanguageOpen ? 'rotate-180' : ''}`}
227
+ />
228
+ </button>
229
+
230
+ {isLanguageOpen && (
231
+ <>
232
+ <div
233
+ className="fixed inset-0 z-10"
234
+ onClick={() => setIsLanguageOpen(false)}
235
+ />
236
+ <div className="absolute right-0 mt-2 w-40 bg-white rounded-lg shadow-lg z-20 py-1" style={{ border: '1px solid #2D5C63' }}>
237
+ {languages.map((lang) => (
238
+ <button
239
+ key={lang.code}
240
+ onClick={() => handleLanguageChange(lang.code)}
241
+ className={`w-full text-left px-4 py-2 text-body-md hover:bg-gray-50 transition-colors ${
242
+ currentLanguage.code === lang.code
243
+ ? 'bg-gray-50 color-brand font-medium'
244
+ : 'color-gray-700'
245
+ }`}
246
+ >
247
+ {lang.name}
248
+ </button>
249
+ ))}
250
+ </div>
251
+ </>
252
+ )}
253
+ </div>
254
+ )}
255
+ </header>
256
+ )
88
257
  );
89
258
  };
90
259
 
@@ -62,3 +62,37 @@ export const SinIdiomas = {
62
62
  ),
63
63
  };
64
64
 
65
+ export const Login = {
66
+ render: () => (
67
+ <HeaderGeneral
68
+ variant="login"
69
+ logoText="HEXA"
70
+ suiteText="Suite"
71
+ languages={[
72
+ { code: 'es-AR', name: 'Español' },
73
+ { code: 'en', name: 'English' },
74
+ ]}
75
+ currentLanguageCode="es-AR"
76
+ onLanguageChange={(langCode) => console.log('Idioma cambiado:', langCode)}
77
+ />
78
+ ),
79
+ };
80
+
81
+ export const LoginConMargenCustom = {
82
+ render: () => (
83
+ <div style={{ '--mf-ds-grid-margin': '32px' }}>
84
+ <HeaderGeneral
85
+ variant="login"
86
+ logoText="HEXA"
87
+ suiteText="Suite"
88
+ languages={[
89
+ { code: 'es-AR', name: 'Español' },
90
+ { code: 'en', name: 'English' },
91
+ ]}
92
+ currentLanguageCode="en"
93
+ onLanguageChange={(langCode) => console.log('Idioma cambiado:', langCode)}
94
+ />
95
+ </div>
96
+ ),
97
+ };
98
+