@axzydev/axzy_ui_system 1.2.0 → 1.2.2

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.
Files changed (202) hide show
  1. package/dist/index.css +82 -197
  2. package/dist/index.css.map +1 -1
  3. package/package.json +2 -2
  4. package/src/App.tsx +354 -0
  5. package/src/assets/logo.png +0 -0
  6. package/src/assets/react.svg +1 -0
  7. package/src/components/alert/alert.props.ts +13 -0
  8. package/src/components/alert/alert.stories.tsx +41 -0
  9. package/src/components/alert/alert.tsx +53 -0
  10. package/src/components/avatar/avatar.props.ts +14 -0
  11. package/src/components/avatar/avatar.stories.tsx +46 -0
  12. package/src/components/avatar/avatar.tsx +53 -0
  13. package/src/components/badget/badget.props.ts +12 -0
  14. package/src/components/badget/badget.stories.tsx +76 -0
  15. package/src/components/badget/badget.tsx +61 -0
  16. package/src/components/breadcrumbs/breadcrumbs.props.ts +13 -0
  17. package/src/components/breadcrumbs/breadcrumbs.stories.tsx +21 -0
  18. package/src/components/breadcrumbs/breadcrumbs.tsx +34 -0
  19. package/src/components/button/button.props.ts +18 -0
  20. package/src/components/button/button.stories.tsx +174 -0
  21. package/src/components/button/button.tsx +117 -0
  22. package/src/components/calendar/calendar.props.ts +33 -0
  23. package/src/components/calendar/calendar.stories.tsx +91 -0
  24. package/src/components/calendar/calendar.tsx +608 -0
  25. package/src/components/calendar/index.ts +3 -0
  26. package/src/components/card/card.props.ts +13 -0
  27. package/src/components/card/card.stories.tsx +58 -0
  28. package/src/components/card/card.tsx +79 -0
  29. package/src/components/checkbox/checkbox.props.ts +11 -0
  30. package/src/components/checkbox/checkbox.stories.tsx +54 -0
  31. package/src/components/checkbox/checkbox.tsx +52 -0
  32. package/src/components/confirm-dialog/confirm-dialog.props.ts +14 -0
  33. package/src/components/confirm-dialog/confirm-dialog.stories.tsx +33 -0
  34. package/src/components/confirm-dialog/confirm-dialog.tsx +45 -0
  35. package/src/components/data-table/ITDataTable.stories.tsx +213 -0
  36. package/src/components/data-table/dataTable.props.ts +69 -0
  37. package/src/components/data-table/dataTable.tsx +313 -0
  38. package/src/components/date-picker/date-picker.props.ts +30 -0
  39. package/src/components/date-picker/date-picker.stories.tsx +90 -0
  40. package/src/components/date-picker/datePicker.tsx +307 -0
  41. package/src/components/dialog/dialog.props.ts +9 -0
  42. package/src/components/dialog/dialog.stories.tsx +80 -0
  43. package/src/components/dialog/dialog.tsx +88 -0
  44. package/src/components/divider/divider.props.ts +8 -0
  45. package/src/components/divider/divider.stories.tsx +34 -0
  46. package/src/components/divider/divider.tsx +21 -0
  47. package/src/components/drawer/drawer.props.ts +14 -0
  48. package/src/components/drawer/drawer.stories.tsx +41 -0
  49. package/src/components/drawer/drawer.tsx +53 -0
  50. package/src/components/dropfile/dropfile.stories.tsx +75 -0
  51. package/src/components/dropfile/dropfile.tsx +407 -0
  52. package/src/components/empty-state/empty-state.props.ts +9 -0
  53. package/src/components/empty-state/empty-state.stories.tsx +20 -0
  54. package/src/components/empty-state/empty-state.tsx +21 -0
  55. package/src/components/flex/flex.props.ts +22 -0
  56. package/src/components/flex/flex.stories.tsx +71 -0
  57. package/src/components/flex/flex.tsx +79 -0
  58. package/src/components/form-builder/fieldRenderer.tsx +218 -0
  59. package/src/components/form-builder/formBuilder.context.tsx +70 -0
  60. package/src/components/form-builder/formBuilder.props.ts +43 -0
  61. package/src/components/form-builder/formBuilder.stories.tsx +317 -0
  62. package/src/components/form-builder/formBuilder.tsx +186 -0
  63. package/src/components/form-builder/useFormBuilder.ts +80 -0
  64. package/src/components/form-header/form-header.props.ts +5 -0
  65. package/src/components/form-header/form-header.tsx +38 -0
  66. package/src/components/grid/grid.props.ts +17 -0
  67. package/src/components/grid/grid.stories.tsx +72 -0
  68. package/src/components/grid/grid.tsx +69 -0
  69. package/src/components/image/image.props.ts +7 -0
  70. package/src/components/image/image.tsx +38 -0
  71. package/src/components/input/input.props.ts +49 -0
  72. package/src/components/input/input.stories.tsx +115 -0
  73. package/src/components/input/input.tsx +615 -0
  74. package/src/components/layout/layout.props.ts +10 -0
  75. package/src/components/layout/layout.stories.tsx +114 -0
  76. package/src/components/layout/layout.tsx +80 -0
  77. package/src/components/loader/loader.props.ts +8 -0
  78. package/src/components/loader/loader.stories.tsx +105 -0
  79. package/src/components/loader/loader.tsx +108 -0
  80. package/src/components/navbar/navbar.props.ts +37 -0
  81. package/src/components/navbar/navbar.tsx +328 -0
  82. package/src/components/page/page.props.ts +19 -0
  83. package/src/components/page/page.stories.tsx +98 -0
  84. package/src/components/page/page.tsx +90 -0
  85. package/src/components/page-header/page-header.props.ts +11 -0
  86. package/src/components/page-header/page-header.stories.tsx +61 -0
  87. package/src/components/page-header/page-header.tsx +62 -0
  88. package/src/components/pagination/pagination.props.ts +53 -0
  89. package/src/components/pagination/pagination.stories.tsx +111 -0
  90. package/src/components/pagination/pagination.tsx +241 -0
  91. package/src/components/popover/popover.props.ts +12 -0
  92. package/src/components/popover/popover.stories.tsx +25 -0
  93. package/src/components/popover/popover.tsx +45 -0
  94. package/src/components/progress/progress.props.ts +12 -0
  95. package/src/components/progress/progress.stories.tsx +40 -0
  96. package/src/components/progress/progress.tsx +52 -0
  97. package/src/components/radio/radio.props.ts +16 -0
  98. package/src/components/radio/radio.stories.tsx +50 -0
  99. package/src/components/radio/radio.tsx +58 -0
  100. package/src/components/search-select/index.ts +2 -0
  101. package/src/components/search-select/search-select.props.ts +46 -0
  102. package/src/components/search-select/search-select.stories.tsx +129 -0
  103. package/src/components/search-select/search-select.tsx +229 -0
  104. package/src/components/searchTable/components/EditableCell.tsx +149 -0
  105. package/src/components/searchTable/components/PaginationControls.tsx +86 -0
  106. package/src/components/searchTable/components/PaginationInfo.tsx +20 -0
  107. package/src/components/searchTable/components/SearchAndSortBar.tsx +53 -0
  108. package/src/components/searchTable/components/SearchInput.tsx +33 -0
  109. package/src/components/searchTable/components/SortButton.tsx +50 -0
  110. package/src/components/searchTable/components/TableEmptyState.tsx +22 -0
  111. package/src/components/searchTable/components/TableHeader.tsx +35 -0
  112. package/src/components/searchTable/components/TableHeaderCell.tsx +43 -0
  113. package/src/components/searchTable/components/TableRow.tsx +144 -0
  114. package/src/components/searchTable/searchTable.props.ts +56 -0
  115. package/src/components/searchTable/searchTable.tsx +187 -0
  116. package/src/components/segmented-control/segmented-control.props.ts +18 -0
  117. package/src/components/segmented-control/segmented-control.stories.tsx +63 -0
  118. package/src/components/segmented-control/segmented-control.tsx +52 -0
  119. package/src/components/select/select.props.ts +25 -0
  120. package/src/components/select/select.stories.tsx +86 -0
  121. package/src/components/select/select.tsx +150 -0
  122. package/src/components/sidebar/sidebar.props.ts +28 -0
  123. package/src/components/sidebar/sidebar.stories.tsx +117 -0
  124. package/src/components/sidebar/sidebar.tsx +313 -0
  125. package/src/components/skeleton/skeleton.props.ts +12 -0
  126. package/src/components/skeleton/skeleton.stories.tsx +30 -0
  127. package/src/components/skeleton/skeleton.tsx +45 -0
  128. package/src/components/slide/slide.props.ts +45 -0
  129. package/src/components/slide/slide.stories.tsx +121 -0
  130. package/src/components/slide/slide.tsx +109 -0
  131. package/src/components/slider/slider.props.ts +10 -0
  132. package/src/components/slider/slider.stories.tsx +30 -0
  133. package/src/components/slider/slider.tsx +49 -0
  134. package/src/components/stack/stack.props.ts +19 -0
  135. package/src/components/stack/stack.stories.tsx +79 -0
  136. package/src/components/stack/stack.tsx +79 -0
  137. package/src/components/stat-card/stat-card.props.ts +13 -0
  138. package/src/components/stat-card/stat-card.stories.tsx +41 -0
  139. package/src/components/stat-card/stat-card.tsx +44 -0
  140. package/src/components/stepper/stepper.css +26 -0
  141. package/src/components/stepper/stepper.props.ts +29 -0
  142. package/src/components/stepper/stepper.stories.tsx +155 -0
  143. package/src/components/stepper/stepper.tsx +227 -0
  144. package/src/components/table/table.props.ts +43 -0
  145. package/src/components/table/table.stories.tsx +189 -0
  146. package/src/components/table/table.tsx +376 -0
  147. package/src/components/tabs/tabs.props.ts +18 -0
  148. package/src/components/tabs/tabs.stories.tsx +32 -0
  149. package/src/components/tabs/tabs.tsx +74 -0
  150. package/src/components/text/text.props.ts +9 -0
  151. package/src/components/text/text.tsx +20 -0
  152. package/src/components/textarea/textarea.props.ts +15 -0
  153. package/src/components/textarea/textarea.stories.tsx +27 -0
  154. package/src/components/textarea/textarea.tsx +55 -0
  155. package/src/components/theme-provider/themeProvider.props.ts +28 -0
  156. package/src/components/theme-provider/themeProvider.tsx +1854 -0
  157. package/src/components/time-picker/timePicker.props.ts +16 -0
  158. package/src/components/time-picker/timePicker.stories.tsx +131 -0
  159. package/src/components/time-picker/timePicker.tsx +317 -0
  160. package/src/components/toast/toast.css +32 -0
  161. package/src/components/toast/toast.props.ts +13 -0
  162. package/src/components/toast/toast.stories.tsx +138 -0
  163. package/src/components/toast/toast.tsx +87 -0
  164. package/src/components/tooltip/tooltip.props.ts +11 -0
  165. package/src/components/tooltip/tooltip.stories.tsx +20 -0
  166. package/src/components/tooltip/tooltip.tsx +55 -0
  167. package/src/components/topbar/topbar.props.ts +21 -0
  168. package/src/components/topbar/topbar.stories.tsx +80 -0
  169. package/src/components/topbar/topbar.tsx +205 -0
  170. package/src/components/triple-filter/tripleFilter.props.ts +15 -0
  171. package/src/components/triple-filter/tripleFilter.stories.tsx +32 -0
  172. package/src/components/triple-filter/tripleFilter.tsx +50 -0
  173. package/src/hooks/useClickOutside.ts +21 -0
  174. package/src/hooks/useDebouncedSearch.ts +55 -0
  175. package/src/hooks/useEditableRow.ts +157 -0
  176. package/src/hooks/useTableState.ts +122 -0
  177. package/src/index.css +168 -0
  178. package/src/index.ts +165 -0
  179. package/src/main.tsx +9 -0
  180. package/src/showcases/DataShowcases.tsx +260 -0
  181. package/src/showcases/FeedbackShowcases.tsx +268 -0
  182. package/src/showcases/FormShowcases.tsx +1159 -0
  183. package/src/showcases/HomeShowcase.tsx +324 -0
  184. package/src/showcases/LayoutPrimitivesShowcases.tsx +569 -0
  185. package/src/showcases/NavigationShowcases.tsx +193 -0
  186. package/src/showcases/PageShowcases.tsx +207 -0
  187. package/src/showcases/ShowcaseLayout.tsx +139 -0
  188. package/src/showcases/StructureShowcases.tsx +152 -0
  189. package/src/types/badget.types.ts +37 -0
  190. package/src/types/button.types.ts +16 -0
  191. package/src/types/colors.types.ts +3 -0
  192. package/src/types/field.types.ts +103 -0
  193. package/src/types/formik.types.ts +15 -0
  194. package/src/types/input.types.ts +14 -0
  195. package/src/types/loader.types.ts +9 -0
  196. package/src/types/sizes.types.ts +1 -0
  197. package/src/types/table.types.ts +15 -0
  198. package/src/types/toast.types.ts +8 -0
  199. package/src/types/yup.types.ts +11 -0
  200. package/src/utils/color.utils.ts +99 -0
  201. package/src/utils/styles.ts +120 -0
  202. package/src/utils/table.utils.ts +10 -0
@@ -0,0 +1,87 @@
1
+ import { positionStyles } from "@/types/toast.types";
2
+ import { theme } from "@/theme/theme";
3
+ import clsx from "clsx";
4
+ import { useEffect, useState } from "react";
5
+ import "./toast.css";
6
+ import { ITToastProps } from "./toast.props";
7
+ import { FaTimesCircle, FaCheckCircle, FaExclamationTriangle, FaInfoCircle, FaTimes } from "react-icons/fa";
8
+ import ITText from "@/components/text/text";
9
+
10
+ export default function ITToast({
11
+ message,
12
+ type = "info",
13
+ duration = 1500,
14
+ position = "top-right",
15
+ onClose,
16
+ }: ITToastProps) {
17
+ const [isVisible, setIsVisible] = useState(true);
18
+
19
+ useEffect(() => {
20
+ const timer = setTimeout(() => {
21
+ setIsVisible(false);
22
+ setTimeout(() => {
23
+ if (onClose) onClose();
24
+ }, 300); // Wait for transition to finish
25
+ }, duration);
26
+
27
+ return () => clearTimeout(timer);
28
+ }, [duration, onClose]);
29
+
30
+ const handleClose = () => {
31
+ setIsVisible(false);
32
+ setTimeout(() => {
33
+ if (onClose) onClose();
34
+ }, 300);
35
+ };
36
+
37
+ // Resolve dynamic theme colors for background
38
+ const isThemeColor = type in theme.colors;
39
+ const backgroundColor = isThemeColor
40
+ ? theme.colors[type as keyof typeof theme.colors][500]
41
+ : theme.colors.primary[500];
42
+
43
+ // Determine Icon based on type
44
+ const TypeIcon = () => {
45
+ switch (type) {
46
+ case "success":
47
+ return <FaCheckCircle className="w-5 h-5 flex-shrink-0" />;
48
+ case "error":
49
+ case "danger":
50
+ return <FaTimesCircle className="w-5 h-5 flex-shrink-0" />;
51
+ case "warning":
52
+ return <FaExclamationTriangle className="w-5 h-5 flex-shrink-0" />;
53
+ case "info":
54
+ default:
55
+ return <FaInfoCircle className="w-5 h-5 flex-shrink-0" />;
56
+ }
57
+ };
58
+
59
+ return (
60
+ <div
61
+ className={clsx(
62
+ "fixed z-50 p-4 rounded-xl shadow-xl flex items-center justify-between gap-4 transition-all duration-300 text-white min-w-[300px]",
63
+ positionStyles[position],
64
+ {
65
+ "opacity-100 translate-y-0 scale-100": isVisible,
66
+ "opacity-0 scale-95": !isVisible,
67
+ "-translate-y-4": !isVisible && position.startsWith("top"),
68
+ "translate-y-4": !isVisible && position.startsWith("bottom"),
69
+ }
70
+ )}
71
+ style={{ backgroundColor }}
72
+ role="alert"
73
+ >
74
+ <div className="flex items-center gap-3">
75
+ <TypeIcon />
76
+ <ITText as="span" className="font-medium text-sm sm:text-base leading-snug">{message}</ITText>
77
+ </div>
78
+ <button
79
+ onClick={handleClose}
80
+ className="p-1.5 rounded-full hover:bg-black/15 transition-colors focus:outline-none focus:ring-2 focus:ring-white/50"
81
+ aria-label="Close notification"
82
+ >
83
+ <FaTimes className="w-4 h-4" />
84
+ </button>
85
+ </div>
86
+ );
87
+ }
@@ -0,0 +1,11 @@
1
+ import { ReactNode } from "react";
2
+
3
+ export type TooltipPosition = "top" | "bottom" | "left" | "right";
4
+
5
+ export interface ITTooltipProps {
6
+ content: ReactNode;
7
+ children: ReactNode;
8
+ position?: TooltipPosition;
9
+ delay?: number;
10
+ className?: string;
11
+ }
@@ -0,0 +1,20 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import ITTooltip from "./tooltip";
3
+ import ITButton from "../button/button";
4
+
5
+ const meta: Meta<typeof ITTooltip> = {
6
+ title: "Components/Overlay/ITTooltip",
7
+ component: ITTooltip,
8
+ tags: ["autodocs"],
9
+ };
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof ITTooltip>;
13
+
14
+ export const Top: Story = {
15
+ args: { content: "Tooltip arriba", position: "top", children: <ITButton label="Hover me" /> },
16
+ };
17
+
18
+ export const Bottom: Story = {
19
+ args: { content: "Tooltip abajo", position: "bottom", children: <ITButton label="Hover me" /> },
20
+ };
@@ -0,0 +1,55 @@
1
+ import { useState } from "react";
2
+ import clsx from "clsx";
3
+ import { ITTooltipProps, TooltipPosition } from "./tooltip.props";
4
+ import ITText from "@/components/text/text";
5
+
6
+ const positionClasses: Record<TooltipPosition, string> = {
7
+ top: "bottom-full left-1/2 -translate-x-1/2 mb-2",
8
+ bottom: "top-full left-1/2 -translate-x-1/2 mt-2",
9
+ left: "right-full top-1/2 -translate-y-1/2 mr-2",
10
+ right: "left-full top-1/2 -translate-y-1/2 ml-2",
11
+ };
12
+
13
+ const arrowClasses: Record<TooltipPosition, string> = {
14
+ top: "top-full left-1/2 -translate-x-1/2 border-l-4 border-r-4 border-t-4 border-transparent border-t-slate-800",
15
+ bottom: "bottom-full left-1/2 -translate-x-1/2 border-l-4 border-r-4 border-b-4 border-transparent border-b-slate-800",
16
+ left: "left-full top-1/2 -translate-y-1/2 border-t-4 border-b-4 border-l-4 border-transparent border-l-slate-800",
17
+ right: "right-full top-1/2 -translate-y-1/2 border-t-4 border-b-4 border-r-4 border-transparent border-r-slate-800",
18
+ };
19
+
20
+ export default function ITTooltip({
21
+ content,
22
+ children,
23
+ position = "top",
24
+ delay = 200,
25
+ className,
26
+ }: ITTooltipProps) {
27
+ const [visible, setVisible] = useState(false);
28
+ let timeout: ReturnType<typeof setTimeout>;
29
+
30
+ const handleMouseEnter = () => {
31
+ timeout = setTimeout(() => setVisible(true), delay);
32
+ };
33
+ const handleMouseLeave = () => {
34
+ clearTimeout(timeout);
35
+ setVisible(false);
36
+ };
37
+
38
+ return (
39
+ <div
40
+ className={clsx("relative inline-flex", className)}
41
+ onMouseEnter={handleMouseEnter}
42
+ onMouseLeave={handleMouseLeave}
43
+ >
44
+ {children}
45
+ {visible && (
46
+ <div className={clsx("absolute z-[200] pointer-events-none", positionClasses[position])}>
47
+ <div className="bg-slate-800 dark:bg-slate-700 text-white text-xs rounded-lg px-2.5 py-1.5 whitespace-nowrap shadow-lg">
48
+ <ITText as="span">{content}</ITText>
49
+ </div>
50
+ <div className={clsx("absolute", arrowClasses[position])} />
51
+ </div>
52
+ )}
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,21 @@
1
+ export interface ITTopBarNavItem {
2
+ id: string;
3
+ label: string;
4
+ icon?: any;
5
+ action: () => void;
6
+ }
7
+
8
+ export interface ITTopBarProps {
9
+ logo?: any;
10
+ logoText?: string;
11
+ userMenu?: {
12
+ userName: string;
13
+ userEmail: string;
14
+ userImage?: string;
15
+ menuItems: { label: string; onClick: () => void }[];
16
+ };
17
+ navItems?: ITTopBarNavItem[];
18
+ onNavItemClick?: (id: string) => void;
19
+ showMobileMenuButton?: boolean;
20
+ onToggleMobileMenu?: () => void;
21
+ }
@@ -0,0 +1,80 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import ITTopBar from './topbar';
3
+ import { FaShieldAlt, FaChartBar, FaUserCog } from 'react-icons/fa';
4
+
5
+ const meta: Meta<typeof ITTopBar> = {
6
+ title: 'Components/Layout & Navigation/ITTopBar',
7
+ component: ITTopBar,
8
+ parameters: {
9
+ layout: 'fullscreen',
10
+ docs: {
11
+ description: {
12
+ component: 'Un Header moderno, con efecto glassmorphism, sombras suaves y un submenú de usuario flotante con diseño premium. Totalmente integrado a theme.ts.',
13
+ },
14
+ },
15
+ },
16
+ tags: ['autodocs'],
17
+ };
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof ITTopBar>;
21
+
22
+ const mockUserMenu = {
23
+ userName: 'Alejandro G.',
24
+ userEmail: 'alejandro@axzy.io',
25
+ userImage: 'https://i.pravatar.cc/150?u=a042581f4e29026024d',
26
+ menuItems: [
27
+ { label: 'Mi Perfil', onClick: () => console.log('Perfil clicked') },
28
+ { label: 'Configuración de Cuenta', onClick: () => console.log('Config clicked') },
29
+ { label: 'Preferencias', onClick: () => console.log('Preferencias clicked') },
30
+ { label: 'Cerrar Sesión', onClick: () => console.log('Logout clicked') },
31
+ ],
32
+ };
33
+
34
+ const mockNavItems = [
35
+ { id: '1', label: 'Monitor', icon: <FaChartBar />, action: () => console.log('Monitor') },
36
+ { id: '2', label: 'Seguridad', icon: <FaShieldAlt />, action: () => console.log('Seguridad') },
37
+ { id: '3', label: 'Admin', icon: <FaUserCog />, action: () => console.log('Admin') },
38
+ ];
39
+
40
+ export const Default: Story = {
41
+ args: {
42
+ logoText: 'AXZY SYSTEM',
43
+ showMobileMenuButton: true,
44
+ userMenu: mockUserMenu,
45
+ navItems: mockNavItems,
46
+ onToggleMobileMenu: () => console.log('Toggle Mobile Menu'),
47
+ },
48
+ render: (args) => (
49
+ <div className="h-[400px] w-full bg-slate-50 overflow-y-auto">
50
+ <ITTopBar {...args} />
51
+ <div className="p-8">
52
+ <h1 className="text-2xl font-bold text-slate-800 mb-4">Contenido de ejemplo</h1>
53
+ <p className="text-slate-500 max-w-2xl leading-relaxed mb-6">
54
+ Haz scroll hacia abajo para ver cómo el ITTopBar mantiene su efecto glassmorphism
55
+ (fondo desenfocado) mientras se superpone al contenido. Prueba también a hacer clic
56
+ en el avatar del usuario a la derecha para ver el nuevo menú desplegable flotante.
57
+ </p>
58
+ {[1, 2, 3, 4, 5].map(i => (
59
+ <div key={i} className="h-24 bg-white rounded-xl border border-slate-200 shadow-sm mb-4"></div>
60
+ ))}
61
+ </div>
62
+ </div>
63
+ )
64
+ };
65
+
66
+ export const WithoutNavigation: Story = {
67
+ args: {
68
+ logoText: 'AXZY Simple',
69
+ showMobileMenuButton: true,
70
+ userMenu: {
71
+ ...mockUserMenu,
72
+ userImage: undefined, // Test without avatar image
73
+ },
74
+ },
75
+ render: (args) => (
76
+ <div className="h-[250px] w-full bg-slate-50">
77
+ <ITTopBar {...args} />
78
+ </div>
79
+ )
80
+ };
@@ -0,0 +1,205 @@
1
+ import { FaUserCircle, FaBars } from "react-icons/fa";
2
+ import useClickOutside from "../../hooks/useClickOutside";
3
+ import { useRef, useState } from "react";
4
+ import { ITTopBarProps } from "./topbar.props";
5
+ import ITText from "@/components/text/text";
6
+
7
+ export default function ITTopBar({
8
+ logo,
9
+ logoText,
10
+ userMenu,
11
+ showMobileMenuButton,
12
+ onToggleMobileMenu,
13
+ navItems,
14
+ onNavItemClick,
15
+ }: ITTopBarProps) {
16
+ const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
17
+ const userMenuRef = useRef<HTMLDivElement>(null);
18
+
19
+ useClickOutside(userMenuRef, () => setIsUserMenuOpen(false));
20
+
21
+ return (
22
+ <header
23
+ className="sticky top-0 z-40 backdrop-blur-md transition-all duration-300"
24
+ style={{
25
+ backgroundColor: "var(--topbar-bg, rgba(255, 255, 255, 0.9))",
26
+ borderBottom: "1px solid var(--topbar-border, #e2e8f0)",
27
+ boxShadow: "none",
28
+ }}
29
+ >
30
+ <div className="flex items-center justify-between h-[72px] px-6 lg:px-8">
31
+
32
+ {/* LEFT AREA: Logo & Mobile Toggle */}
33
+ <div className="flex items-center gap-5">
34
+
35
+ {/* Mobile Sidebar Toggle */}
36
+ {showMobileMenuButton && (
37
+ <button
38
+ className="lg:hidden p-2.5 rounded-xl transition-colors duration-200"
39
+ style={{
40
+ color: "var(--topbar-icon, #64748b)",
41
+ }}
42
+ onMouseEnter={(e) => e.currentTarget.style.backgroundColor = "var(--topbar-user-hover, #f1f5f9)"}
43
+ onMouseLeave={(e) => e.currentTarget.style.backgroundColor = "transparent"}
44
+ onClick={onToggleMobileMenu}
45
+ >
46
+ <FaBars className="w-[1.125rem] h-[1.125rem]" />
47
+ </button>
48
+ )}
49
+
50
+ {/* Logo */}
51
+ <div className="flex items-center gap-3">
52
+ {logo && (
53
+ <div className="flex-shrink-0 drop-shadow-sm">
54
+ {logo}
55
+ </div>
56
+ )}
57
+
58
+ {logoText && (
59
+ <ITText as="span"
60
+ className="text-[1.15rem] font-bold tracking-tight"
61
+ style={{ color: "var(--topbar-text, #0f172a)" }}
62
+ >
63
+ {logoText}
64
+ </ITText>
65
+ )}
66
+ </div>
67
+
68
+ {/* Top Navigation Items (Desktop) */}
69
+ {navItems && navItems.length > 0 && (
70
+ <nav className="hidden md:flex ml-8 space-x-1 border-l pl-8" style={{ borderColor: "var(--topbar-border, #e2e8f0)" }}>
71
+ {navItems.map((item) => (
72
+ <button
73
+ key={item.id}
74
+ onClick={() => onNavItemClick?.(item.id)}
75
+ className="px-4 py-2 rounded-lg font-medium text-[0.9rem] transition-all duration-200 ease-[cubic-bezier(0.2,0,0,1)]"
76
+ style={{ color: "var(--topbar-text, #475569)" }}
77
+ onMouseEnter={(e) => {
78
+ e.currentTarget.style.color = "var(--topbar-text, #0f172a)";
79
+ e.currentTarget.style.backgroundColor = "var(--topbar-user-hover, #f1f5f9)";
80
+ }}
81
+ onMouseLeave={(e) => {
82
+ e.currentTarget.style.color = "var(--topbar-text, #475569)";
83
+ e.currentTarget.style.backgroundColor = "transparent";
84
+ }}
85
+ >
86
+ <div className="flex items-center gap-2">
87
+ {item.icon && <span className="opacity-70">{item.icon}</span>}
88
+ <ITText as="span">{item.label}</ITText>
89
+ </div>
90
+ </button>
91
+ ))}
92
+ </nav>
93
+ )}
94
+ </div>
95
+
96
+ {/* RIGHT AREA: User Menu */}
97
+ {userMenu && (
98
+ <div className="relative">
99
+ <button
100
+ type="button"
101
+ className="flex items-center gap-3 rounded-full pl-2 pr-4 py-1.5 transition-all duration-200 ease-[cubic-bezier(0.2,0,0,1)] border border-transparent hover:border-gray-200"
102
+ style={{
103
+ backgroundColor: isUserMenuOpen ? "var(--topbar-user-hover, #f1f5f9)" : "transparent",
104
+ }}
105
+ onMouseEnter={(e) => {
106
+ if (!isUserMenuOpen) e.currentTarget.style.backgroundColor = "var(--topbar-user-hover, #f1f5f9)";
107
+ }}
108
+ onMouseLeave={(e) => {
109
+ if (!isUserMenuOpen) e.currentTarget.style.backgroundColor = "transparent";
110
+ }}
111
+ onClick={() => setIsUserMenuOpen(!isUserMenuOpen)}
112
+ >
113
+ {/* Avatar */}
114
+ <div className="relative">
115
+ {userMenu.userImage ? (
116
+ <img
117
+ className="w-9 h-9 rounded-full object-cover ring-2 ring-white shadow-sm"
118
+ src={userMenu.userImage}
119
+ alt="Current user"
120
+ />
121
+ ) : (
122
+ <div className="w-9 h-9 rounded-full bg-slate-100 flex items-center justify-center ring-2 ring-white shadow-sm">
123
+ <FaUserCircle className="w-6 h-6" style={{ color: "var(--topbar-icon, #94a3b8)" }} />
124
+ </div>
125
+ )}
126
+ {/* Active dot indicator */}
127
+ <div className="absolute bottom-0 right-0 w-2.5 h-2.5 bg-green-500 border-2 border-white rounded-full"></div>
128
+ </div>
129
+
130
+ {/* Name Details */}
131
+ <div className="hidden sm:flex flex-col text-left py-0.5">
132
+ <ITText as="span"
133
+ className="font-semibold text-[0.85rem] leading-tight"
134
+ style={{ color: "var(--topbar-user-text, #0f172a)" }}
135
+ >
136
+ {userMenu.userName}
137
+ </ITText>
138
+ <ITText as="span"
139
+ className="text-[0.7rem] font-medium"
140
+ style={{ color: "var(--topbar-user-subtitle, #64748b)" }}
141
+ >
142
+ {userMenu.userEmail}
143
+ </ITText>
144
+ </div>
145
+ </button>
146
+
147
+ {/* Dropdown Menu */}
148
+ <div
149
+ ref={userMenuRef}
150
+ className={`
151
+ absolute right-0 mt-3 w-64 rounded-2xl shadow-[0_10px_40px_-10px_rgba(0,0,0,0.15)] z-50 overflow-hidden transform origin-top-right transition-all duration-200 ease-[cubic-bezier(0.2,0,0,1)]
152
+ ${isUserMenuOpen ? "scale-100 opacity-100 translate-y-0" : "scale-95 opacity-0 -translate-y-2 pointer-events-none"}
153
+ `}
154
+ style={{
155
+ backgroundColor: "var(--topbar-user-dropdown-bg, #ffffff)",
156
+ border: "1px solid var(--topbar-user-dropdown-border, #f1f5f9)"
157
+ }}
158
+ >
159
+ {/* Dropdown Header */}
160
+ <div className="px-5 py-4 border-b" style={{ borderColor: "var(--topbar-user-dropdown-border, #f1f5f9)", backgroundColor: "var(--topbar-user-bg, #f8fafc)" }}>
161
+ <ITText as="span" className="block font-bold text-[0.9rem]" style={{ color: "var(--topbar-user-text, #0f172a)" }}>
162
+ {userMenu.userName}
163
+ </ITText>
164
+ <ITText as="span" className="block text-xs font-medium truncate mt-0.5" style={{ color: "var(--topbar-user-subtitle, #64748b)" }}>
165
+ {userMenu.userEmail}
166
+ </ITText>
167
+ </div>
168
+
169
+ {/* Dropdown Items */}
170
+ <ul className="py-2">
171
+ {userMenu.menuItems.map((m, i) => {
172
+ const isDestructive = m.label.toLowerCase().includes('salir') || m.label.toLowerCase().includes('cerrar') || m.label.toLowerCase().includes('logout');
173
+
174
+ return (
175
+ <li key={i} className="px-2">
176
+ {i === userMenu.menuItems.length - 1 && isDestructive && i > 0 && (
177
+ <div className="h-px bg-slate-100 my-1 mx-2"></div>
178
+ )}
179
+ <button
180
+ onClick={(e) => {
181
+ m.onClick();
182
+ setIsUserMenuOpen(false);
183
+ }}
184
+ className="block w-full text-left px-3 py-2.5 rounded-xl text-[0.875rem] font-medium transition-colors duration-150"
185
+ style={{ color: isDestructive ? '#ef4444' : "var(--topbar-user-text, #334155)" }}
186
+ onMouseEnter={(e) => {
187
+ e.currentTarget.style.backgroundColor = isDestructive ? '#fef2f2' : "var(--topbar-user-item-hover, #f8fafc)";
188
+ }}
189
+ onMouseLeave={(e) => {
190
+ e.currentTarget.style.backgroundColor = "transparent";
191
+ }}
192
+ >
193
+ <ITText as="span">{m.label}</ITText>
194
+ </button>
195
+ </li>
196
+ )
197
+ })}
198
+ </ul>
199
+ </div>
200
+ </div>
201
+ )}
202
+ </div>
203
+ </header>
204
+ );
205
+ }
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import { ColorsTypes } from "@/types/colors.types";
3
+
4
+ export interface ITTripleFilterOption<T> {
5
+ label: string;
6
+ value: T;
7
+ }
8
+
9
+ export interface ITTripleFilterProps<T> {
10
+ value: T;
11
+ onChange: (value: T) => void;
12
+ options: ITTripleFilterOption<T>[];
13
+ color?: ColorsTypes;
14
+ className?: string;
15
+ }
@@ -0,0 +1,32 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import ITTripleFilter from './tripleFilter';
3
+
4
+ const meta: Meta<typeof ITTripleFilter> = {
5
+ title: 'Components/ITTripleFilter',
6
+ component: ITTripleFilter,
7
+ tags: ['autodocs'],
8
+ };
9
+
10
+ export default meta;
11
+ type Story = StoryObj<typeof ITTripleFilter>;
12
+
13
+ export const Default: Story = {
14
+ args: {
15
+ value: 'all',
16
+ options: [
17
+ { label: 'Todos', value: 'all' },
18
+ { label: 'Activos', value: 'active' },
19
+ { label: 'Inactivos', value: 'inactive' },
20
+ ],
21
+ },
22
+ };
23
+
24
+ export const Boolean: Story = {
25
+ args: {
26
+ value: true,
27
+ options: [
28
+ { label: 'Sí', value: true },
29
+ { label: 'No', value: false },
30
+ ],
31
+ },
32
+ };
@@ -0,0 +1,50 @@
1
+ import clsx from "clsx";
2
+ import { ITTripleFilterProps } from "./tripleFilter.props";
3
+ import { ColorsTypes } from "@/types/colors.types";
4
+ import ITText from "@/components/text/text";
5
+
6
+ const colorMap: Record<ColorsTypes, string> = {
7
+ primary: "text-primary-600",
8
+ secondary: "text-secondary-600",
9
+ success: "text-success-600",
10
+ danger: "text-danger-600",
11
+ warning: "text-warning-600",
12
+ info: "text-info-600",
13
+ purple: "text-purple-600",
14
+ error: "text-danger-600",
15
+ gray: "text-secondary-600",
16
+ };
17
+
18
+ /**
19
+ * @description Generic triple/segmented filter component with color support.
20
+ */
21
+ export const ITTripleFilter = <T extends string | boolean>({
22
+ value,
23
+ onChange,
24
+ options,
25
+ color = "primary",
26
+ className,
27
+ }: ITTripleFilterProps<T>) => {
28
+ return (
29
+ <div
30
+ className={clsx("flex bg-slate-100 p-1 rounded-xl gap-1 w-fit", className)}
31
+ >
32
+ {options.map((option) => (
33
+ <button
34
+ key={String(option.value)}
35
+ onClick={() => onChange(option.value)}
36
+ className={clsx(
37
+ "px-4 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-wider transition-all duration-200 whitespace-nowrap",
38
+ value === option.value
39
+ ? clsx("bg-white shadow-sm", colorMap[color])
40
+ : "text-slate-400 hover:text-slate-600"
41
+ )}
42
+ >
43
+ <ITText as="span">{option.label}</ITText>
44
+ </button>
45
+ ))}
46
+ </div>
47
+ );
48
+ };
49
+
50
+ export default ITTripleFilter;
@@ -0,0 +1,21 @@
1
+ import { useEffect } from "react";
2
+
3
+ const useClickOutside = (
4
+ ref: React.RefObject<HTMLElement>,
5
+ callback: () => void
6
+ ) => {
7
+ useEffect(() => {
8
+ const handleClickOutside = (event: MouseEvent) => {
9
+ if (ref.current && !ref.current.contains(event.target as Node)) {
10
+ callback();
11
+ }
12
+ };
13
+
14
+ document.addEventListener("mousedown", handleClickOutside);
15
+ return () => {
16
+ document.removeEventListener("mousedown", handleClickOutside);
17
+ };
18
+ }, [ref, callback]);
19
+ };
20
+
21
+ export default useClickOutside;
@@ -0,0 +1,55 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+
3
+ interface UseDebouncedSearchOptions {
4
+ initialValue?: string;
5
+ debounceMs?: number;
6
+ onSearch: (value: string) => void;
7
+ }
8
+
9
+ interface UseDebouncedSearchResult {
10
+ searchTerm: string;
11
+ setSearchTerm: (value: string) => void;
12
+ handleSearchChange: (value: string) => void;
13
+ handleClearSearch: () => void;
14
+ }
15
+
16
+ export function useDebouncedSearch({
17
+ initialValue = "",
18
+ debounceMs = 500,
19
+ onSearch,
20
+ }: UseDebouncedSearchOptions): UseDebouncedSearchResult {
21
+ const [searchTerm, setSearchTerm] = useState(initialValue);
22
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
23
+
24
+ const debouncedSearch = useCallback(
25
+ (value: string) => {
26
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
27
+ timeoutRef.current = setTimeout(() => {
28
+ onSearch(value);
29
+ }, debounceMs);
30
+ },
31
+ [onSearch, debounceMs]
32
+ );
33
+
34
+ const handleSearchChange = useCallback(
35
+ (value: string) => {
36
+ setSearchTerm(value);
37
+ debouncedSearch(value);
38
+ },
39
+ [debouncedSearch]
40
+ );
41
+
42
+ const handleClearSearch = useCallback(() => {
43
+ setSearchTerm("");
44
+ onSearch("");
45
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
46
+ }, [onSearch]);
47
+
48
+ useEffect(() => {
49
+ return () => {
50
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
51
+ };
52
+ }, []);
53
+
54
+ return { searchTerm, setSearchTerm, handleSearchChange, handleClearSearch };
55
+ }