anima-ds-nucleus 1.0.11 → 1.0.12

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.11",
3
+ "version": "1.0.12",
4
4
  "description": "Anima Design System - A comprehensive React component library",
5
5
  "author": "Nucleus Labs <ipvasallo@nucleus.com.ar>",
6
6
  "license": "UNLICENSED",
@@ -1,4 +1,4 @@
1
- import { useState } from 'react';
1
+ import { useEffect, useRef, useState } from 'react';
2
2
  import { Icon } from '../../Atoms/Icon/Icon';
3
3
  import { Avatar } from '../../Atoms/Avatar/Avatar';
4
4
  import { Typography } from '../../Atoms/Typography/Typography';
@@ -6,12 +6,23 @@ import { SidebarCoreMobile } from '../Sidebar/SidebarCore';
6
6
 
7
7
  export const HeaderCore = ({
8
8
  searchPlaceholder = 'Buscar empleados, reportes, configuraciones...',
9
+ mobileSearchPlaceholder = 'Busca lo que necesites',
9
10
  userName,
10
11
  userAvatar,
11
12
  notificationCount = 0,
12
13
  onSearch,
13
14
  onNotificationClick,
14
15
  onUserClick,
16
+ // Borde inferior (subrayado)
17
+ showBottomBorder = false,
18
+ bottomBorderClassName = 'border-b border-gray-200',
19
+ // Sidebar mobile (animación/cierre controlado)
20
+ mobileSidebarTransitionDurationMs = 300,
21
+ mobileSidebarTransitionEasing = 'ease-in-out',
22
+ // Mobile background
23
+ mobileBackgroundColor = '#F3F3F3',
24
+ mobileHeaderClassName = '',
25
+ mobileHeaderStyle,
15
26
  // Overrides de estilo/clases para poder ajustar layout desde el proyecto consumidor
16
27
  desktopLayoutClassName = '',
17
28
  desktopLayoutStyle,
@@ -52,6 +63,8 @@ export const HeaderCore = ({
52
63
  sidebarSections,
53
64
  sidebarActiveItem,
54
65
  onSidebarItemClick,
66
+ // Mobile: permitir persistir el item activo aunque el consumidor no controle sidebarActiveItem
67
+ enableInternalSidebarActiveItem = true,
55
68
  sidebarItemBadges,
56
69
  sidebarCompanyName,
57
70
  sidebarCompanyLogo,
@@ -64,6 +77,48 @@ export const HeaderCore = ({
64
77
  }) => {
65
78
  const [searchValue, setSearchValue] = useState('');
66
79
  const [isSidebarOpen, setIsSidebarOpen] = useState(false);
80
+ const [shouldRenderSidebarMobile, setShouldRenderSidebarMobile] = useState(false);
81
+ const [internalSidebarActiveItem, setInternalSidebarActiveItem] = useState(undefined);
82
+ const closeTimeoutRef = useRef(null);
83
+
84
+ const openSidebarMobile = () => {
85
+ if (closeTimeoutRef.current) {
86
+ clearTimeout(closeTimeoutRef.current);
87
+ closeTimeoutRef.current = null;
88
+ }
89
+ // Para que se vea la animación de apertura:
90
+ // 1) montamos el sidebar en estado cerrado
91
+ // 2) lo abrimos en el siguiente frame (después de pintar)
92
+ setShouldRenderSidebarMobile(true);
93
+ setIsSidebarOpen(false);
94
+ requestAnimationFrame(() => setIsSidebarOpen(true));
95
+ };
96
+
97
+ const closeSidebarMobile = () => {
98
+ setIsSidebarOpen(false);
99
+ };
100
+
101
+ const resolvedSidebarActiveItem =
102
+ sidebarActiveItem !== undefined && sidebarActiveItem !== null
103
+ ? sidebarActiveItem
104
+ : enableInternalSidebarActiveItem
105
+ ? internalSidebarActiveItem
106
+ : sidebarActiveItem;
107
+
108
+ useEffect(() => {
109
+ if (!isSidebarOpen && shouldRenderSidebarMobile) {
110
+ closeTimeoutRef.current = setTimeout(() => {
111
+ setShouldRenderSidebarMobile(false);
112
+ closeTimeoutRef.current = null;
113
+ }, mobileSidebarTransitionDurationMs);
114
+ }
115
+ return () => {
116
+ if (closeTimeoutRef.current) {
117
+ clearTimeout(closeTimeoutRef.current);
118
+ closeTimeoutRef.current = null;
119
+ }
120
+ };
121
+ }, [isSidebarOpen, shouldRenderSidebarMobile, mobileSidebarTransitionDurationMs]);
67
122
 
68
123
  const handleSearchChange = (e) => {
69
124
  const value = e.target.value;
@@ -82,7 +137,7 @@ export const HeaderCore = ({
82
137
 
83
138
  return (
84
139
  <header
85
- className={`bg-white border-b border-gray-200 ${className}`}
140
+ className={`bg-white ${showBottomBorder ? bottomBorderClassName : ''} ${className}`}
86
141
  {...props}
87
142
  >
88
143
  <style>{`
@@ -222,10 +277,10 @@ export const HeaderCore = ({
222
277
  opacity: 1,
223
278
  ...(notificationDesktopIconWrapperStyle || {}),
224
279
  }}
225
- >
280
+ >
226
281
  <Icon
227
- name="BellAlertIcon"
228
- variant="24-outline"
282
+ name="BellIcon"
283
+ variant="24-outline"
229
284
  size={notificationDesktopIconSize}
230
285
  strokeWidth={notificationDesktopIconStrokeWidth}
231
286
  className={`color-gray-600 ${notificationIconClassName}`}
@@ -240,7 +295,7 @@ export const HeaderCore = ({
240
295
  ...(notificationIconStyle || {}),
241
296
  ...(notificationDesktopIconStyle || {}),
242
297
  }}
243
- />
298
+ />
244
299
  </span>
245
300
  {notificationCount > 0 && (
246
301
  <span
@@ -318,14 +373,17 @@ export const HeaderCore = ({
318
373
  </div>
319
374
 
320
375
  {/* Layout Mobile (768px o menos) */}
321
- <div className="header-mobile-layout">
376
+ <div
377
+ className={`header-mobile-layout ${mobileHeaderClassName}`}
378
+ style={{ background: mobileBackgroundColor, ...(mobileHeaderStyle || {}) }}
379
+ >
322
380
  {/* Primera fila: Menú, Core, Notificaciones y Avatar */}
323
381
  <div className="header-mobile-top-row">
324
382
  {/* Lado izquierdo: Menú hamburguesa y Core */}
325
383
  <div className="flex items-center space-x-2">
326
384
  {/* Botón menú hamburguesa */}
327
385
  <button
328
- onClick={() => setIsSidebarOpen(true)}
386
+ onClick={openSidebarMobile}
329
387
  className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
330
388
  aria-label="Menú"
331
389
  >
@@ -381,10 +439,10 @@ export const HeaderCore = ({
381
439
  opacity: 1,
382
440
  ...(notificationMobileIconWrapperStyle || {}),
383
441
  }}
384
- >
442
+ >
385
443
  <Icon
386
- name="BellAlertIcon"
387
- variant="24-outline"
444
+ name="BellIcon"
445
+ variant="24-outline"
388
446
  size={notificationMobileIconSize}
389
447
  strokeWidth={notificationMobileIconStrokeWidth}
390
448
  className={`color-gray-600 ${notificationIconClassName}`}
@@ -404,7 +462,7 @@ export const HeaderCore = ({
404
462
  {showNotificationBadgeOnMobile && notificationCount > 0 && (
405
463
  <span
406
464
  className={`h-5 text-white flex items-center justify-center text-body-sm font-medium ${notificationBadgeClassName}`}
407
- style={{
465
+ style={{
408
466
  backgroundColor: '#6D3856',
409
467
  minWidth: '24px',
410
468
  width: 'fit-content',
@@ -455,7 +513,7 @@ export const HeaderCore = ({
455
513
  type="text"
456
514
  value={searchValue}
457
515
  onChange={handleSearchChange}
458
- placeholder={searchPlaceholder}
516
+ placeholder={mobileSearchPlaceholder}
459
517
  className="header-search-input flex-1 pl-3 pr-3 py-2 bg-white border border-gray-400 rounded-l-lg
460
518
  focus:outline-none text-sm"
461
519
  style={{
@@ -495,17 +553,21 @@ export const HeaderCore = ({
495
553
  </div>
496
554
  </div>
497
555
 
498
- {/* SidebarCoreMobile - Solo visible en móvil cuando está abierto */}
499
- {isSidebarOpen && (
556
+ {/* SidebarCoreMobile - mantener montado para animación de cierre */}
557
+ {shouldRenderSidebarMobile && (
500
558
  <div className="header-core-mobile-sidebar-wrapper">
501
559
  <SidebarCoreMobile
502
560
  sections={sidebarSections}
503
- activeItem={sidebarActiveItem}
561
+ activeItem={resolvedSidebarActiveItem}
504
562
  onItemClick={(itemId) => {
563
+ if (enableInternalSidebarActiveItem) {
564
+ setInternalSidebarActiveItem(itemId);
565
+ }
505
566
  if (onSidebarItemClick) {
506
567
  onSidebarItemClick(itemId);
507
568
  }
508
- setIsSidebarOpen(false);
569
+ // permitir que el item se "pinte" y luego cerrar con animación
570
+ requestAnimationFrame(() => closeSidebarMobile());
509
571
  }}
510
572
  itemBadges={sidebarItemBadges}
511
573
  companyName={sidebarCompanyName}
@@ -515,7 +577,9 @@ export const HeaderCore = ({
515
577
  coreContainerStyle={sidebarCoreContainerStyle}
516
578
  coreTextStyle={sidebarCoreTextStyle}
517
579
  isOpen={isSidebarOpen}
518
- onClose={() => setIsSidebarOpen(false)}
580
+ onClose={closeSidebarMobile}
581
+ transitionDurationMs={mobileSidebarTransitionDurationMs}
582
+ transitionEasing={mobileSidebarTransitionEasing}
519
583
  />
520
584
  </div>
521
585
  )}
@@ -0,0 +1,262 @@
1
+ import { Typography } from '../../Atoms/Typography/Typography';
2
+ import { Icon } from '../../Atoms/Icon/Icon';
3
+
4
+ const defaultDateOptions = {
5
+ weekday: 'long',
6
+ day: '2-digit',
7
+ month: 'long',
8
+ year: 'numeric',
9
+ };
10
+
11
+ const coerceDate = (value) => {
12
+ if (!value) return null;
13
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
14
+ const d = new Date(value);
15
+ return Number.isNaN(d.getTime()) ? null : d;
16
+ };
17
+
18
+ const capitalizeFirst = (value) => {
19
+ if (!value) return value;
20
+ return value.charAt(0).toUpperCase() + value.slice(1);
21
+ };
22
+
23
+ /**
24
+ * SaludoConFechaDashboard
25
+ *
26
+ * Encabezado de dashboard con saludo y fecha formateada (locale-aware).
27
+ * Todas las medidas/estilos se pueden overridear por props para que el proyecto consumidor ajuste sin tocar la librería.
28
+ */
29
+ export const SaludoConFechaDashboard = ({
30
+ // Content
31
+ userName,
32
+ saludo = 'Hola',
33
+ date = new Date(),
34
+ showDate = true,
35
+
36
+ // Breadcrumb / inicio (arriba del saludo)
37
+ showInicio = true,
38
+ inicioText = 'Inicio',
39
+ showInicioIcon = true,
40
+ inicioIconName = 'ChevronRightIcon',
41
+ inicioIconVariant = '20-solid', // heroicons "mini"
42
+ inicioIconSize = 16,
43
+ // Responsive behavior (<= breakpoint): centrar saludo y ocultar el resto
44
+ responsiveBreakpointPx = 768,
45
+ responsiveHideInicio = true,
46
+ responsiveHideDate = true,
47
+ responsiveCenterGreeting = true,
48
+
49
+ // Date formatting
50
+ locale = 'es-AR',
51
+ dateOptions = defaultDateOptions,
52
+ formatDate, // (date, { locale, dateOptions }) => string
53
+ capitalizeDate = true,
54
+
55
+ // Layout / styles (overrideable)
56
+ className = '',
57
+ style,
58
+ leftClassName = '',
59
+ leftStyle,
60
+ rightClassName = '',
61
+ rightStyle,
62
+ saludoFechaContainerClassName = '',
63
+ saludoFechaContainerStyle,
64
+ inicioContainerClassName = '',
65
+ inicioContainerStyle,
66
+ inicioTextClassName = '',
67
+ inicioTextStyle,
68
+ inicioIconClassName = '',
69
+ inicioIconStyle,
70
+ greetingClassName = '',
71
+ greetingStyle,
72
+ greetingPrefix = '¡',
73
+ greetingSuffix = '!',
74
+ nameClassName = '',
75
+ nameStyle,
76
+ dateTextClassName = '',
77
+ dateTextStyle,
78
+ ...props
79
+ }) => {
80
+ const resolvedDate = showDate ? coerceDate(date) : null;
81
+
82
+ let formattedDate = '';
83
+ if (resolvedDate) {
84
+ if (typeof formatDate === 'function') {
85
+ formattedDate = formatDate(resolvedDate, { locale, dateOptions });
86
+ } else {
87
+ formattedDate = new Intl.DateTimeFormat(locale, dateOptions).format(resolvedDate);
88
+ }
89
+ if (capitalizeDate) {
90
+ formattedDate = capitalizeFirst(formattedDate);
91
+ }
92
+ }
93
+
94
+ return (
95
+ <div
96
+ className={`w-full ${className}`}
97
+ style={style}
98
+ {...props}
99
+ >
100
+ <style>{`
101
+ @media (max-width: ${responsiveBreakpointPx}px) {
102
+ .scfd-inicio {
103
+ display: ${responsiveHideInicio ? 'none' : 'flex'};
104
+ }
105
+ .scfd-date {
106
+ display: ${responsiveHideDate ? 'none' : 'flex'};
107
+ }
108
+ .scfd-saludo-fecha {
109
+ justify-content: ${responsiveCenterGreeting ? 'center' : 'space-between'};
110
+ }
111
+ .scfd-greeting-wrapper {
112
+ width: 100%;
113
+ }
114
+ .scfd-greeting {
115
+ text-align: ${responsiveCenterGreeting ? 'center' : 'left'};
116
+ }
117
+ }
118
+ `}</style>
119
+ {/* Grid de 2 filas:
120
+ - Fila 1: "Inicio >" (solo izquierda)
121
+ - Fila 2: "Hola, Nombre" (izquierda) + Fecha (derecha) alineados a la misma altura */}
122
+ <div className="grid grid-cols-[minmax(0,1fr)_auto] grid-rows-[auto_auto] gap-x-4 gap-y-1 items-center">
123
+ {showInicio && (
124
+ <div
125
+ className={`scfd-inicio col-start-1 row-start-1 flex items-center gap-1 ${inicioContainerClassName}`}
126
+ style={{
127
+ // Mantener "Inicio" alineado al mismo borde que "Hola" (padding-left del contenedor inferior)
128
+ paddingLeft: '8px',
129
+ ...(inicioContainerStyle || {}),
130
+ }}
131
+ >
132
+ <Typography
133
+ variant="body-md"
134
+ className={inicioTextClassName}
135
+ style={{
136
+ color: '#223B40',
137
+ fontFamily: 'IBM Plex Sans',
138
+ fontWeight: 700,
139
+ fontStyle: 'normal',
140
+ fontSize: '14px',
141
+ lineHeight: '20px',
142
+ letterSpacing: '0%',
143
+ verticalAlign: 'middle',
144
+ ...(inicioTextStyle || {}),
145
+ }}
146
+ >
147
+ {inicioText}
148
+ </Typography>
149
+ {showInicioIcon && inicioIconName ? (
150
+ <Icon
151
+ name={inicioIconName}
152
+ variant={inicioIconVariant}
153
+ size={inicioIconSize}
154
+ className={inicioIconClassName}
155
+ style={{
156
+ color: '#223B40',
157
+ opacity: 1,
158
+ transform: 'rotate(0deg)',
159
+ ...(inicioIconStyle || {}),
160
+ }}
161
+ />
162
+ ) : null}
163
+ </div>
164
+ )}
165
+
166
+ {/* Contenedor padre de la fila "Hola + Fecha" (Figma) */}
167
+ <div
168
+ className={`scfd-saludo-fecha col-start-1 col-end-3 row-start-2 flex items-center justify-between ${saludoFechaContainerClassName}`}
169
+ style={{
170
+ // Responsive: ocupar el ancho disponible sin generar scroll horizontal,
171
+ // manteniendo el ancho Figma como máximo.
172
+ width: '100%',
173
+ maxWidth: '1250px',
174
+ minHeight: '74px',
175
+ gap: '24px',
176
+ opacity: 1,
177
+ transform: 'rotate(0deg)',
178
+ paddingTop: '16px',
179
+ paddingBottom: '16px',
180
+ paddingLeft: '8px',
181
+ boxSizing: 'border-box',
182
+ ...(saludoFechaContainerStyle || {}),
183
+ }}
184
+ >
185
+ <div className={`scfd-greeting-wrapper min-w-0 ${leftClassName}`} style={leftStyle}>
186
+ <Typography
187
+ variant="h5"
188
+ className={`scfd-greeting ${greetingClassName}`}
189
+ style={{
190
+ color: '#223B40',
191
+ fontFamily: 'IBM Plex Sans',
192
+ fontWeight: 400,
193
+ fontStyle: 'normal',
194
+ fontSize: '28px',
195
+ lineHeight: '42px',
196
+ letterSpacing: '0%',
197
+ verticalAlign: 'middle',
198
+ ...(greetingStyle || {}),
199
+ }}
200
+ >
201
+ <span
202
+ style={{
203
+ display: 'inline-block',
204
+ verticalAlign: 'middle',
205
+ fontFamily: 'IBM Plex Sans',
206
+ fontWeight: 400,
207
+ fontStyle: 'normal',
208
+ fontSize: '28px',
209
+ lineHeight: '42px',
210
+ letterSpacing: '0%',
211
+ color: '#223B40',
212
+ }}
213
+ >
214
+ {greetingPrefix}
215
+ {saludo}
216
+ </span>
217
+ {userName ? (
218
+ <>
219
+ <span style={{ display: 'inline-block', verticalAlign: 'middle' }}>{',\u00A0'}</span>
220
+ <span
221
+ className={nameClassName}
222
+ style={{
223
+ display: 'inline-block',
224
+ verticalAlign: 'middle',
225
+ color: '#223B40',
226
+ fontFamily: 'IBM Plex Sans',
227
+ fontWeight: 400,
228
+ fontStyle: 'normal',
229
+ fontSize: '28px',
230
+ lineHeight: '42px',
231
+ letterSpacing: '0%',
232
+ ...(nameStyle || {}),
233
+ }}
234
+ >
235
+ {userName}
236
+ </span>
237
+ </>
238
+ ) : null}
239
+ <span style={{ display: 'inline-block', verticalAlign: 'middle' }}>{greetingSuffix}</span>
240
+ </Typography>
241
+ </div>
242
+
243
+ {showDate && formattedDate ? (
244
+ <div className={`scfd-date flex items-center flex-shrink-0 ${rightClassName}`} style={rightStyle}>
245
+ <Typography
246
+ variant="body-lg"
247
+ className={`color-gray-600 ${dateTextClassName}`}
248
+ style={dateTextStyle}
249
+ >
250
+ {formattedDate}
251
+ </Typography>
252
+ </div>
253
+ ) : null}
254
+ </div>
255
+ </div>
256
+ </div>
257
+ );
258
+ };
259
+
260
+ export default SaludoConFechaDashboard;
261
+
262
+
@@ -0,0 +1,64 @@
1
+ import { SaludoConFechaDashboard } from './SaludoConFechaDashboard';
2
+
3
+ export default {
4
+ title: 'Layout/SaludoConFechaDashboard',
5
+ component: SaludoConFechaDashboard,
6
+ tags: ['autodocs'],
7
+ argTypes: {
8
+ locale: { control: 'text' },
9
+ saludo: { control: 'text' },
10
+ userName: { control: 'text' },
11
+ showDate: { control: 'boolean' },
12
+ capitalizeDate: { control: 'boolean' },
13
+ showInicio: { control: 'boolean' },
14
+ inicioText: { control: 'text' },
15
+ showInicioIcon: { control: 'boolean' },
16
+ inicioIconName: { control: 'text' },
17
+ },
18
+ };
19
+
20
+ export const Default = {
21
+ args: {
22
+ saludo: 'Hola',
23
+ userName: 'Juan',
24
+ date: new Date(),
25
+ locale: 'es-AR',
26
+ showDate: true,
27
+ showInicio: true,
28
+ inicioText: 'Inicio',
29
+ showInicioIcon: true,
30
+ inicioIconName: 'ChevronRightIcon',
31
+ },
32
+ };
33
+
34
+ export const SinNombre = {
35
+ args: {
36
+ saludo: 'Bienvenido',
37
+ userName: '',
38
+ date: new Date(),
39
+ locale: 'es-AR',
40
+ },
41
+ };
42
+
43
+ export const Portugues = {
44
+ args: {
45
+ saludo: 'Olá',
46
+ userName: 'Mariana',
47
+ date: new Date(),
48
+ locale: 'pt-BR',
49
+ },
50
+ };
51
+
52
+ export const EstilosCustom = {
53
+ args: {
54
+ saludo: 'Hola',
55
+ userName: 'Dashboard',
56
+ date: new Date(),
57
+ className: 'p-4 bg-white rounded-lg border border-gray-200',
58
+ rightClassName: 'bg-gray-50 px-3 py-1 rounded-lg border border-gray-200',
59
+ dateTextClassName: 'color-gray-700',
60
+ inicioContainerClassName: 'opacity-80',
61
+ },
62
+ };
63
+
64
+