@axzydev/axzy_ui_system 1.2.1 → 1.2.3

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 (207) hide show
  1. package/dist/index.cjs +1 -1
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +82 -1
  4. package/dist/index.css.map +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/package.json +2 -2
  8. package/src/App.tsx +354 -0
  9. package/src/assets/logo.png +0 -0
  10. package/src/assets/react.svg +1 -0
  11. package/src/components/alert/alert.props.ts +13 -0
  12. package/src/components/alert/alert.stories.tsx +41 -0
  13. package/src/components/alert/alert.tsx +53 -0
  14. package/src/components/avatar/avatar.props.ts +14 -0
  15. package/src/components/avatar/avatar.stories.tsx +46 -0
  16. package/src/components/avatar/avatar.tsx +53 -0
  17. package/src/components/badget/badget.props.ts +12 -0
  18. package/src/components/badget/badget.stories.tsx +76 -0
  19. package/src/components/badget/badget.tsx +61 -0
  20. package/src/components/breadcrumbs/breadcrumbs.props.ts +13 -0
  21. package/src/components/breadcrumbs/breadcrumbs.stories.tsx +21 -0
  22. package/src/components/breadcrumbs/breadcrumbs.tsx +34 -0
  23. package/src/components/button/button.props.ts +18 -0
  24. package/src/components/button/button.stories.tsx +174 -0
  25. package/src/components/button/button.tsx +117 -0
  26. package/src/components/calendar/calendar.props.ts +33 -0
  27. package/src/components/calendar/calendar.stories.tsx +91 -0
  28. package/src/components/calendar/calendar.tsx +608 -0
  29. package/src/components/calendar/index.ts +3 -0
  30. package/src/components/card/card.props.ts +13 -0
  31. package/src/components/card/card.stories.tsx +58 -0
  32. package/src/components/card/card.tsx +79 -0
  33. package/src/components/checkbox/checkbox.props.ts +11 -0
  34. package/src/components/checkbox/checkbox.stories.tsx +54 -0
  35. package/src/components/checkbox/checkbox.tsx +52 -0
  36. package/src/components/confirm-dialog/confirm-dialog.props.ts +14 -0
  37. package/src/components/confirm-dialog/confirm-dialog.stories.tsx +33 -0
  38. package/src/components/confirm-dialog/confirm-dialog.tsx +45 -0
  39. package/src/components/data-table/ITDataTable.stories.tsx +213 -0
  40. package/src/components/data-table/dataTable.props.ts +69 -0
  41. package/src/components/data-table/dataTable.tsx +313 -0
  42. package/src/components/date-picker/date-picker.props.ts +30 -0
  43. package/src/components/date-picker/date-picker.stories.tsx +90 -0
  44. package/src/components/date-picker/datePicker.tsx +307 -0
  45. package/src/components/dialog/dialog.props.ts +9 -0
  46. package/src/components/dialog/dialog.stories.tsx +80 -0
  47. package/src/components/dialog/dialog.tsx +88 -0
  48. package/src/components/divider/divider.props.ts +8 -0
  49. package/src/components/divider/divider.stories.tsx +34 -0
  50. package/src/components/divider/divider.tsx +21 -0
  51. package/src/components/drawer/drawer.props.ts +14 -0
  52. package/src/components/drawer/drawer.stories.tsx +41 -0
  53. package/src/components/drawer/drawer.tsx +53 -0
  54. package/src/components/dropfile/dropfile.stories.tsx +75 -0
  55. package/src/components/dropfile/dropfile.tsx +407 -0
  56. package/src/components/empty-state/empty-state.props.ts +9 -0
  57. package/src/components/empty-state/empty-state.stories.tsx +20 -0
  58. package/src/components/empty-state/empty-state.tsx +21 -0
  59. package/src/components/flex/flex.props.ts +22 -0
  60. package/src/components/flex/flex.stories.tsx +71 -0
  61. package/src/components/flex/flex.tsx +79 -0
  62. package/src/components/form-builder/fieldRenderer.tsx +218 -0
  63. package/src/components/form-builder/formBuilder.context.tsx +70 -0
  64. package/src/components/form-builder/formBuilder.props.ts +43 -0
  65. package/src/components/form-builder/formBuilder.stories.tsx +317 -0
  66. package/src/components/form-builder/formBuilder.tsx +186 -0
  67. package/src/components/form-builder/useFormBuilder.ts +80 -0
  68. package/src/components/form-header/form-header.props.ts +5 -0
  69. package/src/components/form-header/form-header.tsx +38 -0
  70. package/src/components/grid/grid.props.ts +17 -0
  71. package/src/components/grid/grid.stories.tsx +72 -0
  72. package/src/components/grid/grid.tsx +69 -0
  73. package/src/components/image/image.props.ts +7 -0
  74. package/src/components/image/image.tsx +38 -0
  75. package/src/components/input/input.props.ts +49 -0
  76. package/src/components/input/input.stories.tsx +115 -0
  77. package/src/components/input/input.tsx +615 -0
  78. package/src/components/layout/layout.props.ts +10 -0
  79. package/src/components/layout/layout.stories.tsx +114 -0
  80. package/src/components/layout/layout.tsx +80 -0
  81. package/src/components/loader/loader.props.ts +8 -0
  82. package/src/components/loader/loader.stories.tsx +105 -0
  83. package/src/components/loader/loader.tsx +108 -0
  84. package/src/components/navbar/navbar.props.ts +37 -0
  85. package/src/components/navbar/navbar.tsx +328 -0
  86. package/src/components/page/page.props.ts +19 -0
  87. package/src/components/page/page.stories.tsx +98 -0
  88. package/src/components/page/page.tsx +90 -0
  89. package/src/components/page-header/page-header.props.ts +11 -0
  90. package/src/components/page-header/page-header.stories.tsx +61 -0
  91. package/src/components/page-header/page-header.tsx +62 -0
  92. package/src/components/pagination/pagination.props.ts +53 -0
  93. package/src/components/pagination/pagination.stories.tsx +111 -0
  94. package/src/components/pagination/pagination.tsx +241 -0
  95. package/src/components/popover/popover.props.ts +12 -0
  96. package/src/components/popover/popover.stories.tsx +25 -0
  97. package/src/components/popover/popover.tsx +45 -0
  98. package/src/components/progress/progress.props.ts +12 -0
  99. package/src/components/progress/progress.stories.tsx +40 -0
  100. package/src/components/progress/progress.tsx +52 -0
  101. package/src/components/radio/radio.props.ts +16 -0
  102. package/src/components/radio/radio.stories.tsx +50 -0
  103. package/src/components/radio/radio.tsx +58 -0
  104. package/src/components/search-select/index.ts +2 -0
  105. package/src/components/search-select/search-select.props.ts +46 -0
  106. package/src/components/search-select/search-select.stories.tsx +129 -0
  107. package/src/components/search-select/search-select.tsx +229 -0
  108. package/src/components/searchTable/components/EditableCell.tsx +149 -0
  109. package/src/components/searchTable/components/PaginationControls.tsx +86 -0
  110. package/src/components/searchTable/components/PaginationInfo.tsx +20 -0
  111. package/src/components/searchTable/components/SearchAndSortBar.tsx +53 -0
  112. package/src/components/searchTable/components/SearchInput.tsx +33 -0
  113. package/src/components/searchTable/components/SortButton.tsx +50 -0
  114. package/src/components/searchTable/components/TableEmptyState.tsx +22 -0
  115. package/src/components/searchTable/components/TableHeader.tsx +35 -0
  116. package/src/components/searchTable/components/TableHeaderCell.tsx +43 -0
  117. package/src/components/searchTable/components/TableRow.tsx +144 -0
  118. package/src/components/searchTable/searchTable.props.ts +56 -0
  119. package/src/components/searchTable/searchTable.tsx +187 -0
  120. package/src/components/segmented-control/segmented-control.props.ts +18 -0
  121. package/src/components/segmented-control/segmented-control.stories.tsx +63 -0
  122. package/src/components/segmented-control/segmented-control.tsx +52 -0
  123. package/src/components/select/select.props.ts +25 -0
  124. package/src/components/select/select.stories.tsx +86 -0
  125. package/src/components/select/select.tsx +150 -0
  126. package/src/components/sidebar/sidebar.props.ts +28 -0
  127. package/src/components/sidebar/sidebar.stories.tsx +117 -0
  128. package/src/components/sidebar/sidebar.tsx +313 -0
  129. package/src/components/skeleton/skeleton.props.ts +12 -0
  130. package/src/components/skeleton/skeleton.stories.tsx +30 -0
  131. package/src/components/skeleton/skeleton.tsx +45 -0
  132. package/src/components/slide/slide.props.ts +45 -0
  133. package/src/components/slide/slide.stories.tsx +121 -0
  134. package/src/components/slide/slide.tsx +109 -0
  135. package/src/components/slider/slider.props.ts +10 -0
  136. package/src/components/slider/slider.stories.tsx +30 -0
  137. package/src/components/slider/slider.tsx +49 -0
  138. package/src/components/stack/stack.props.ts +19 -0
  139. package/src/components/stack/stack.stories.tsx +79 -0
  140. package/src/components/stack/stack.tsx +79 -0
  141. package/src/components/stat-card/stat-card.props.ts +13 -0
  142. package/src/components/stat-card/stat-card.stories.tsx +41 -0
  143. package/src/components/stat-card/stat-card.tsx +44 -0
  144. package/src/components/stepper/stepper.css +26 -0
  145. package/src/components/stepper/stepper.props.ts +29 -0
  146. package/src/components/stepper/stepper.stories.tsx +155 -0
  147. package/src/components/stepper/stepper.tsx +227 -0
  148. package/src/components/table/table.props.ts +43 -0
  149. package/src/components/table/table.stories.tsx +189 -0
  150. package/src/components/table/table.tsx +376 -0
  151. package/src/components/tabs/tabs.props.ts +18 -0
  152. package/src/components/tabs/tabs.stories.tsx +32 -0
  153. package/src/components/tabs/tabs.tsx +74 -0
  154. package/src/components/text/text.props.ts +9 -0
  155. package/src/components/text/text.tsx +20 -0
  156. package/src/components/textarea/textarea.props.ts +15 -0
  157. package/src/components/textarea/textarea.stories.tsx +27 -0
  158. package/src/components/textarea/textarea.tsx +55 -0
  159. package/src/components/theme-provider/themeProvider.props.ts +28 -0
  160. package/src/components/theme-provider/themeProvider.tsx +1854 -0
  161. package/src/components/time-picker/timePicker.props.ts +16 -0
  162. package/src/components/time-picker/timePicker.stories.tsx +131 -0
  163. package/src/components/time-picker/timePicker.tsx +317 -0
  164. package/src/components/toast/toast.css +32 -0
  165. package/src/components/toast/toast.props.ts +13 -0
  166. package/src/components/toast/toast.stories.tsx +138 -0
  167. package/src/components/toast/toast.tsx +87 -0
  168. package/src/components/tooltip/tooltip.props.ts +11 -0
  169. package/src/components/tooltip/tooltip.stories.tsx +20 -0
  170. package/src/components/tooltip/tooltip.tsx +55 -0
  171. package/src/components/topbar/topbar.props.ts +21 -0
  172. package/src/components/topbar/topbar.stories.tsx +80 -0
  173. package/src/components/topbar/topbar.tsx +205 -0
  174. package/src/components/triple-filter/tripleFilter.props.ts +15 -0
  175. package/src/components/triple-filter/tripleFilter.stories.tsx +32 -0
  176. package/src/components/triple-filter/tripleFilter.tsx +50 -0
  177. package/src/dev.css +2 -0
  178. package/src/hooks/useClickOutside.ts +21 -0
  179. package/src/hooks/useDebouncedSearch.ts +55 -0
  180. package/src/hooks/useEditableRow.ts +157 -0
  181. package/src/hooks/useTableState.ts +122 -0
  182. package/src/index.css +168 -0
  183. package/src/index.ts +165 -0
  184. package/src/main.tsx +9 -0
  185. package/src/showcases/DataShowcases.tsx +260 -0
  186. package/src/showcases/FeedbackShowcases.tsx +268 -0
  187. package/src/showcases/FormShowcases.tsx +1159 -0
  188. package/src/showcases/HomeShowcase.tsx +324 -0
  189. package/src/showcases/LayoutPrimitivesShowcases.tsx +569 -0
  190. package/src/showcases/NavigationShowcases.tsx +193 -0
  191. package/src/showcases/PageShowcases.tsx +207 -0
  192. package/src/showcases/ShowcaseLayout.tsx +139 -0
  193. package/src/showcases/StructureShowcases.tsx +152 -0
  194. package/src/types/badget.types.ts +37 -0
  195. package/src/types/button.types.ts +16 -0
  196. package/src/types/colors.types.ts +3 -0
  197. package/src/types/field.types.ts +103 -0
  198. package/src/types/formik.types.ts +15 -0
  199. package/src/types/input.types.ts +14 -0
  200. package/src/types/loader.types.ts +9 -0
  201. package/src/types/sizes.types.ts +1 -0
  202. package/src/types/table.types.ts +15 -0
  203. package/src/types/toast.types.ts +8 -0
  204. package/src/types/yup.types.ts +11 -0
  205. package/src/utils/color.utils.ts +99 -0
  206. package/src/utils/styles.ts +120 -0
  207. package/src/utils/table.utils.ts +10 -0
@@ -0,0 +1,58 @@
1
+ import clsx from "clsx";
2
+ import { ITRadioGroupProps } from "./radio.props";
3
+ import ITText from "@/components/text/text";
4
+
5
+ export default function ITRadioGroup({
6
+ name,
7
+ value,
8
+ onChange,
9
+ options,
10
+ disabled = false,
11
+ direction = "column",
12
+ className,
13
+ }: ITRadioGroupProps) {
14
+ return (
15
+ <div
16
+ className={clsx(
17
+ "flex gap-3",
18
+ direction === "row" ? "flex-row flex-wrap" : "flex-col",
19
+ className
20
+ )}
21
+ >
22
+ {options.map((opt) => {
23
+ const isSelected = opt.value === value;
24
+ return (
25
+ <label
26
+ key={opt.value}
27
+ className={clsx(
28
+ "inline-flex items-center gap-2 cursor-pointer select-none",
29
+ disabled && "opacity-50 cursor-not-allowed"
30
+ )}
31
+ >
32
+ <input
33
+ type="radio"
34
+ name={name}
35
+ value={opt.value}
36
+ checked={isSelected}
37
+ onChange={() => onChange(opt.value)}
38
+ disabled={disabled}
39
+ className="peer sr-only"
40
+ />
41
+ <div
42
+ className={clsx(
43
+ "w-4 h-4 rounded-full border-2 flex items-center justify-center transition-all",
44
+ isSelected
45
+ ? "border-primary-500"
46
+ : "border-slate-300 dark:border-slate-600",
47
+ !disabled && "peer-focus:ring-2 peer-focus:ring-primary-200",
48
+ )}
49
+ >
50
+ {isSelected && <div className="w-2 h-2 rounded-full bg-primary-500" />}
51
+ </div>
52
+ <ITText as="span" className="text-sm text-slate-700 dark:text-slate-300">{opt.label}</ITText>
53
+ </label>
54
+ );
55
+ })}
56
+ </div>
57
+ );
58
+ }
@@ -0,0 +1,2 @@
1
+ export { default as ITSearchSelect } from "./search-select";
2
+ export * from "./search-select.props";
@@ -0,0 +1,46 @@
1
+ import { FocusEvent } from "react";
2
+
3
+ export interface ITSearchSelectOption {
4
+ label: string;
5
+ value: string | number;
6
+ [key: string]: any;
7
+ }
8
+
9
+ export interface ITSearchSelectProps {
10
+ /** Nombre del campo para integraciones con formularios */
11
+ name?: string;
12
+ /** Etiqueta que se muestra arriba del select */
13
+ label?: string;
14
+ /** Texto que se muestra cuando no hay nada seleccionado */
15
+ placeholder?: string;
16
+ /** Valor seleccionado */
17
+ value?: string | number;
18
+ /** Arreglo de opciones (Modo 1: Lista estática) */
19
+ options?: ITSearchSelectOption[];
20
+ /** Campo que se usará como valor (por defecto "value") */
21
+ valueField?: string;
22
+ /** Campo que se usará como etiqueta (por defecto "label") */
23
+ labelField?: string;
24
+ /** Callback cuando cambia el valor */
25
+ onChange?: (value: string | number, option?: ITSearchSelectOption) => void;
26
+ /** Callback cuando pierde el foco */
27
+ onBlur?: (e: FocusEvent<any>) => void;
28
+ /** Indica si el componente está deshabilitado */
29
+ disabled?: boolean;
30
+ /** Clase CSS adicional para el contenedor */
31
+ className?: string;
32
+ /** Indica si el campo ha sido tocado (para validaciones) */
33
+ touched?: boolean;
34
+ /** Indica si el campo es requerido */
35
+ required?: boolean;
36
+ /** Mensaje de error */
37
+ error?: string | boolean;
38
+ /** Indica si el campo es de solo lectura */
39
+ readOnly?: boolean;
40
+ /** Callback para búsqueda en servidor (Modo 2: Conexión con API) */
41
+ onSearch?: (query: string) => void;
42
+ /** Indica si se está cargando información desde la API */
43
+ isLoading?: boolean;
44
+ /** Mensaje cuando no hay resultados */
45
+ noResultsMessage?: string;
46
+ }
@@ -0,0 +1,129 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import ITSearchSelect from "./search-select";
3
+ import { useState } from "react";
4
+ import { ITSearchSelectOption } from "./search-select.props";
5
+
6
+ const meta: Meta<typeof ITSearchSelect> = {
7
+ title: "Components/Form Elements/ITSearchSelect",
8
+ component: ITSearchSelect,
9
+ parameters: {
10
+ layout: "centered",
11
+ },
12
+ tags: ["autodocs"],
13
+ argTypes: {
14
+ disabled: { control: "boolean" },
15
+ required: { control: "boolean" },
16
+ error: { control: "text" },
17
+ isLoading: { control: "boolean" },
18
+ },
19
+ };
20
+
21
+ export default meta;
22
+ type Story = StoryObj<typeof ITSearchSelect>;
23
+
24
+ const options: ITSearchSelectOption[] = [
25
+ { value: 1, label: "Juan Pérez" },
26
+ { value: 2, label: "María García" },
27
+ { value: 3, label: "Carlos Rodríguez" },
28
+ { value: 4, label: "Ana Martínez" },
29
+ { value: 5, label: "Luis López" },
30
+ { value: 6, label: "Elena Sánchez" },
31
+ { value: 7, label: "Roberto Díaz" },
32
+ { value: 8, label: "Marta Castro" },
33
+ ];
34
+
35
+ const SearchSelectWrapper = (args: any) => {
36
+ const [value, setValue] = useState(args.value || "");
37
+ const [touched, setTouched] = useState(false);
38
+
39
+ return (
40
+ <div className="w-[400px]">
41
+ <ITSearchSelect
42
+ {...args}
43
+ value={value}
44
+ onChange={(val) => setValue(val)}
45
+ onBlur={() => setTouched(true)}
46
+ touched={touched}
47
+ />
48
+ </div>
49
+ );
50
+ };
51
+
52
+ export const Default: Story = {
53
+ render: (args) => <SearchSelectWrapper {...args} />,
54
+ args: {
55
+ name: "search-select",
56
+ options: options,
57
+ placeholder: "Busca un usuario...",
58
+ },
59
+ };
60
+
61
+ export const WithLabel: Story = {
62
+ render: (args) => <SearchSelectWrapper {...args} />,
63
+ args: {
64
+ name: "search-select",
65
+ label: "Seleccionar Usuario",
66
+ options: options,
67
+ placeholder: "Busca un usuario...",
68
+ required: true,
69
+ },
70
+ };
71
+
72
+ export const RemoteAPI: Story = {
73
+ render: (args) => {
74
+ const [remoteOptions, setRemoteOptions] = useState<ITSearchSelectOption[]>([]);
75
+ const [loading, setLoading] = useState(false);
76
+ const [val, setVal] = useState<string | number>("");
77
+
78
+ const handleSearch = (query: string) => {
79
+ setLoading(true);
80
+ // Simular llamada a API
81
+ setTimeout(() => {
82
+ const results = options.filter(o =>
83
+ o.label.toLowerCase().includes(query.toLowerCase())
84
+ );
85
+ setRemoteOptions(results);
86
+ setLoading(false);
87
+ }, 1000);
88
+ };
89
+
90
+ return (
91
+ <div className="w-[400px]">
92
+ <ITSearchSelect
93
+ {...args}
94
+ value={val}
95
+ options={remoteOptions}
96
+ isLoading={loading}
97
+ onSearch={handleSearch}
98
+ onChange={(v) => setVal(v)}
99
+ />
100
+ </div>
101
+ );
102
+ },
103
+ args: {
104
+ name: "remote-search",
105
+ label: "Búsqueda en API (Simulada)",
106
+ placeholder: "Escribe para buscar...",
107
+ },
108
+ };
109
+
110
+ export const WithError: Story = {
111
+ render: (args) => <SearchSelectWrapper {...args} />,
112
+ args: {
113
+ name: "search-select",
114
+ label: "Campo con Error",
115
+ options: options,
116
+ error: "Este campo es obligatorio",
117
+ touched: true,
118
+ },
119
+ };
120
+
121
+ export const Disabled: Story = {
122
+ render: (args) => <SearchSelectWrapper {...args} />,
123
+ args: {
124
+ name: "search-select",
125
+ label: "Campo Deshabilitado",
126
+ options: options,
127
+ disabled: true,
128
+ },
129
+ };
@@ -0,0 +1,229 @@
1
+ import React, { useState, useEffect, useRef, useMemo } from "react";
2
+ import clsx from "clsx";
3
+ import { FaAngleDown, FaSearch, FaTimes } from "react-icons/fa";
4
+ import { ITSearchSelectProps, ITSearchSelectOption } from "./search-select.props";
5
+ import { theme } from "@/theme/theme";
6
+ import ITText from "@/components/text/text";
7
+
8
+ /**
9
+ * ITSearchSelect - Un componente de selección con buscador integrado.
10
+ * Soporta filtrado local y búsqueda remota via API.
11
+ */
12
+ export default function ITSearchSelect({
13
+ name,
14
+ options = [],
15
+ label,
16
+ placeholder = "Selecciona una opción",
17
+ valueField = "value",
18
+ labelField = "label",
19
+ value,
20
+ onChange,
21
+ onBlur,
22
+ disabled = false,
23
+ className,
24
+ touched,
25
+ required,
26
+ error,
27
+ readOnly = false,
28
+ onSearch,
29
+ isLoading = false,
30
+ noResultsMessage = "No se encontraron resultados",
31
+ }: ITSearchSelectProps) {
32
+ const [isOpen, setIsOpen] = useState(false);
33
+ const [searchTerm, setSearchTerm] = useState("");
34
+ const [isFocused, setIsFocused] = useState(false);
35
+ const [localTouched, setLocalTouched] = useState(false);
36
+ const containerRef = useRef<HTMLDivElement>(null);
37
+ const timeoutRef = useRef<NodeJS.Timeout | null>(null);
38
+
39
+ // Encontrar la opción seleccionada inicialmente
40
+ const selectedOption = useMemo(() => {
41
+ return options.find((opt) => opt[valueField] === value);
42
+ }, [options, value, valueField]);
43
+
44
+ // Sincronizar el searchTerm con el label de la opción seleccionada si no se está editando
45
+ useEffect(() => {
46
+ if (!isFocused) {
47
+ setSearchTerm(selectedOption ? String(selectedOption[labelField]) : "");
48
+ }
49
+ }, [selectedOption, isFocused, labelField]);
50
+
51
+ // Cerrar el dropdown al hacer click afuera
52
+ useEffect(() => {
53
+ function handleClickOutside(event: MouseEvent) {
54
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
55
+ setIsOpen(false);
56
+ }
57
+ }
58
+ document.addEventListener("mousedown", handleClickOutside);
59
+ return () => document.removeEventListener("mousedown", handleClickOutside);
60
+ }, []);
61
+
62
+ // Filtrado local de opciones (Modo 1)
63
+ const filteredOptions = useMemo(() => {
64
+ if (onSearch) return options; // Modo API
65
+ if (!searchTerm || !isFocused) return options;
66
+ return options.filter((opt) =>
67
+ String(opt[labelField]).toLowerCase().includes(searchTerm.toLowerCase())
68
+ );
69
+ }, [options, searchTerm, onSearch, labelField, isFocused]);
70
+
71
+ // Manejar cambio en el input
72
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
73
+ const query = e.target.value;
74
+ setSearchTerm(query);
75
+ setIsOpen(true);
76
+
77
+ if (onSearch) {
78
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
79
+ timeoutRef.current = setTimeout(() => {
80
+ onSearch(query);
81
+ }, 500);
82
+ }
83
+ };
84
+
85
+ const handleSelect = (option: ITSearchSelectOption) => {
86
+ if (onChange) {
87
+ onChange(option[valueField], option);
88
+ }
89
+ setSearchTerm(String(option[labelField]));
90
+ setIsOpen(false);
91
+ };
92
+
93
+ const handleFocus = () => {
94
+ if (disabled || readOnly) return;
95
+ setIsFocused(true);
96
+ setIsOpen(true);
97
+ // Opcional: borrar el texto al entrar para facilitar la búsqueda
98
+ // setSearchTerm("");
99
+ };
100
+
101
+ const handleInputBlur = (e: React.FocusEvent<HTMLInputElement>) => {
102
+ // Retrasar el cierre para permitir el click en la opción
103
+ setTimeout(() => {
104
+ setIsFocused(false);
105
+ setLocalTouched(true);
106
+ onBlur?.(e);
107
+ }, 200);
108
+ };
109
+
110
+ // Theme logic
111
+ const inputTheme = (theme as any).input || {};
112
+
113
+ const isTouched = touched !== undefined ? touched : localTouched;
114
+ const isEmpty = value === undefined || value === null || String(value).trim() === "";
115
+
116
+ const effectiveError = error !== undefined && error !== false
117
+ ? (error === true ? "Este campo es requerido" : error)
118
+ : (required && isEmpty ? "Este campo es requerido" : undefined);
119
+
120
+ const hasError = isTouched && !!effectiveError;
121
+ const errorMessage = typeof effectiveError === "string" ? effectiveError : "Este campo es requerido";
122
+
123
+ const getInputStyle = () => {
124
+ const style: React.CSSProperties = {
125
+ backgroundColor: inputTheme.backgroundColor || "#ffffff",
126
+ borderColor: inputTheme.borderColor || "#e2e8f0",
127
+ borderRadius: inputTheme.borderRadius || "0.5rem",
128
+ padding: inputTheme.padding || "0.5rem 0.75rem",
129
+ fontSize: inputTheme.fontSize || "0.875rem",
130
+ borderWidth: '1px',
131
+ borderStyle: 'solid',
132
+ transition: 'all 0.2s',
133
+ color: 'var(--input-text-color, var(--color-secondary-900))',
134
+ width: '100%',
135
+ };
136
+
137
+ if (disabled) {
138
+ style.backgroundColor = inputTheme.disabled?.backgroundColor || "#f1f5f9";
139
+ style.borderColor = inputTheme.disabled?.borderColor || "#e2e8f0";
140
+ style.opacity = 0.7;
141
+ style.cursor = "not-allowed";
142
+ }
143
+
144
+ if (hasError) {
145
+ style.borderColor = inputTheme.error?.borderColor || 'red';
146
+ if (isFocused) {
147
+ style.boxShadow = inputTheme.error?.ring;
148
+ }
149
+ } else if (isFocused && !readOnly) {
150
+ style.boxShadow = inputTheme.focus?.ring;
151
+ style.borderColor = inputTheme.focus?.borderColor;
152
+ }
153
+
154
+ return style;
155
+ };
156
+
157
+ return (
158
+ <div className={clsx("w-full flex flex-col gap-1.5", className, isOpen && "relative z-30")} ref={containerRef}>
159
+ {label && (
160
+ <ITText
161
+ as="label"
162
+ className={clsx("text-sm font-medium text-gray-700 dark:text-slate-300", {
163
+ "text-red-500": hasError,
164
+ })}
165
+ >
166
+ <ITText as="span">{label}</ITText>
167
+ {required && <ITText as="span" className="text-red-500 ml-1">*</ITText>}
168
+ </ITText>
169
+ )}
170
+
171
+ <div className="relative">
172
+ <div className="relative flex items-center">
173
+ <input
174
+ type="text"
175
+ name={name}
176
+ value={searchTerm}
177
+ onChange={handleInputChange}
178
+ onFocus={handleFocus}
179
+ onBlur={handleInputBlur}
180
+ disabled={disabled}
181
+ readOnly={readOnly}
182
+ placeholder={placeholder}
183
+ className="outline-none pr-10"
184
+ style={getInputStyle()}
185
+ autoComplete="off"
186
+ />
187
+ <div className="absolute right-3 flex items-center gap-2 text-gray-400 pointer-events-none">
188
+ {isLoading && <div className="animate-spin h-4 w-4 border-2 border-primary-500 border-t-transparent rounded-full" />}
189
+ {!isLoading && <FaSearch size={14} className={clsx({ "text-primary-500": isFocused })} />}
190
+ </div>
191
+ </div>
192
+
193
+ {/* Dropdown Panel */}
194
+ {isOpen && (
195
+ <div className="absolute z-50 w-full mt-1 bg-white dark:bg-slate-900 border border-gray-200 dark:border-slate-800 rounded-lg shadow-xl overflow-hidden animate-in fade-in zoom-in duration-200 origin-top">
196
+ <div className="max-h-60 overflow-y-auto">
197
+ {filteredOptions.length > 0 ? (
198
+ filteredOptions.map((option) => (
199
+ <ITText
200
+ as="div"
201
+ key={option[valueField]}
202
+ onClick={() => handleSelect(option)}
203
+ className={clsx(
204
+ "px-4 py-2 text-sm cursor-pointer transition-colors",
205
+ value === option[valueField]
206
+ ? "bg-primary-50 dark:bg-primary-950/40 text-primary-700 dark:text-primary-300 font-medium"
207
+ : "hover:bg-gray-50 dark:hover:bg-slate-800 text-gray-700 dark:text-slate-300"
208
+ )}
209
+ >
210
+ <ITText as="span">{option[labelField]}</ITText>
211
+ </ITText>
212
+ ))
213
+ ) : (
214
+ <ITText as="div" className="px-4 py-6 text-sm text-center text-gray-500 italic">
215
+ {isLoading ? "Cargando..." : noResultsMessage}
216
+ </ITText>
217
+ )}
218
+ </div>
219
+ </div>
220
+ )}
221
+ </div>
222
+
223
+ {/* Error Message */}
224
+ {hasError && (
225
+ <ITText as="p" className="text-red-500 text-xs mt-1">{errorMessage}</ITText>
226
+ )}
227
+ </div>
228
+ );
229
+ }
@@ -0,0 +1,149 @@
1
+ import React from "react";
2
+ import { SearchColumn } from "../searchTable.props";
3
+ import ITInput from "@/components/input/input";
4
+ import ITSelect from "@/components/select/select";
5
+ import ITDatePicker from "@/components/date-picker/datePicker";
6
+ import ITText from "@/components/text/text";
7
+
8
+ interface EditableCellProps<T> {
9
+ column: SearchColumn<T>;
10
+ value: any;
11
+ onChange: (value: any, error?: string) => void;
12
+ error?: string;
13
+ row: T;
14
+ }
15
+
16
+ export default function EditableCell<T>({
17
+ column,
18
+ value,
19
+ onChange,
20
+ error,
21
+ row,
22
+ }: EditableCellProps<T>) {
23
+ const validate = (val: any) => {
24
+ if (column.validation) return column.validation(val);
25
+ return undefined;
26
+ };
27
+
28
+ const normalizeAndNotify = (input: any) => {
29
+ let newValue = input;
30
+
31
+ if (input && typeof input === "object" && "target" in input) {
32
+ const t = input.target as HTMLInputElement;
33
+ if (column.inputType === "checkbox") newValue = t.checked;
34
+ else newValue = t.value;
35
+ }
36
+
37
+ if (column.inputType === "number") {
38
+ newValue = newValue === "" || newValue === null ? "" : Number(newValue);
39
+ }
40
+
41
+ const errorMsg = validate(newValue);
42
+ onChange(newValue, errorMsg);
43
+ };
44
+
45
+ const handleBlur = () => {
46
+ const errorMsg = validate(value);
47
+ onChange(value, errorMsg);
48
+ };
49
+
50
+ // Funciones de renderizado centralizadas
51
+ const renderITInput = (type: "text" | "number") => {
52
+ return (
53
+ <ITInput
54
+ type={type}
55
+ name={column.key}
56
+ value={type === "number" ? value ?? "" : value || ""}
57
+ onChange={normalizeAndNotify}
58
+ onBlur={handleBlur}
59
+ className="w-full"
60
+ error={error}
61
+ />
62
+ );
63
+ };
64
+
65
+ const renderITDatePicker = () => {
66
+ return (
67
+ <ITDatePicker
68
+ name={column.key}
69
+ value={value || null}
70
+ onChange={normalizeAndNotify}
71
+ onBlur={handleBlur}
72
+ className="w-full"
73
+ error={error}
74
+ />
75
+ );
76
+ };
77
+
78
+ const renderITSelect = (
79
+ options: { value: any; label: string | number }[]
80
+ ) => {
81
+ return (
82
+ <ITSelect
83
+ name={column.key}
84
+ options={options as any}
85
+ value={value}
86
+ onChange={normalizeAndNotify}
87
+ onBlur={handleBlur as any}
88
+ className="w-full"
89
+ error={error}
90
+ />
91
+ );
92
+ };
93
+
94
+ const renderBooleanInput = () => {
95
+ return (
96
+ <ITInput
97
+ type="checkbox"
98
+ name={column.key}
99
+ checked={!!value}
100
+ onChange={(e) => {
101
+ normalizeAndNotify(e.target.checked);
102
+ }}
103
+ onBlur={handleBlur}
104
+ className="w-full"
105
+ error={error}
106
+ />
107
+ );
108
+ };
109
+
110
+ // Función principal que decide qué componente renderizar
111
+ const renderInput = () => {
112
+ const inputType = column.inputType || column.type;
113
+
114
+ switch (inputType) {
115
+ case "number":
116
+ return renderITInput("number");
117
+
118
+ case "select":
119
+ return renderITSelect(column.options || []);
120
+
121
+ case "checkbox":
122
+ case "boolean":
123
+ return renderBooleanInput();
124
+
125
+ case "date":
126
+ return renderITDatePicker();
127
+
128
+ case "catalog":
129
+ if (column.catalogOptions) {
130
+ const options = column.catalogOptions.data.map((item) => ({
131
+ value: item[column.catalogOptions.key || "id"],
132
+ label: item[column.catalogOptions.label || "name"],
133
+ }));
134
+ return renderITSelect(options);
135
+ }
136
+ return renderITInput("text"); // fallback si no hay catalogOptions
137
+
138
+ default:
139
+ return renderITInput("text");
140
+ }
141
+ };
142
+
143
+ return (
144
+ <div className="w-full">
145
+ {renderInput()}
146
+ {error && <div className="text-red-500 text-xs mt-1"><ITText as="span">{error}</ITText></div>}
147
+ </div>
148
+ );
149
+ }
@@ -0,0 +1,86 @@
1
+ import React from "react";
2
+ import { FaArrowLeft, FaArrowRight } from "react-icons/fa";
3
+ import PaginationInfo from "./PaginationInfo";
4
+ import ITSelect from "@/components/select/select";
5
+ import ITButton from "@/components/button/button";
6
+ import ITText from "@/components/text/text";
7
+
8
+ interface PaginationControlsProps {
9
+ pageIndex: number;
10
+ totalPages: number;
11
+ hasPreviousPage: boolean;
12
+ hasNextPage: boolean;
13
+ onPageChange: (page: number) => void;
14
+ itemsPerPage: number;
15
+ itemsPerPageOptions: number[];
16
+ onItemsPerPageChange: (value: number) => void;
17
+ currentCount: number;
18
+ totalCount: number;
19
+ }
20
+
21
+ export default function PaginationControls({
22
+ pageIndex,
23
+ totalPages,
24
+ hasPreviousPage,
25
+ hasNextPage,
26
+ onPageChange,
27
+ itemsPerPage,
28
+ itemsPerPageOptions,
29
+ onItemsPerPageChange,
30
+ currentCount,
31
+ totalCount,
32
+ }: PaginationControlsProps) {
33
+ return (
34
+ <div className="flex flex-col sm:flex-row justify-between items-center gap-4">
35
+ <div className="flex items-center space-x-4">
36
+ <PaginationInfo currentCount={currentCount} totalCount={totalCount} />
37
+
38
+ <div className="flex items-center space-x-2">
39
+ <ITText as="span" className="text-sm text-gray-700">Mostrar:</ITText>
40
+ <ITSelect
41
+ name="itemsPerPage"
42
+ options={itemsPerPageOptions.map((option) => ({
43
+ value: String(option),
44
+ label: String(option),
45
+ }))}
46
+ value={String(itemsPerPage)}
47
+ onChange={(e) => onItemsPerPageChange(Number(e.target.value))}
48
+ onBlur={() => {}}
49
+ size="small"
50
+ className="w-20"
51
+ />
52
+ </div>
53
+ </div>
54
+
55
+ <div className="flex items-center space-x-2">
56
+ <ITButton
57
+ color="secondary"
58
+ size="small"
59
+ onClick={() => onPageChange(pageIndex - 1)}
60
+ disabled={!hasPreviousPage}
61
+ className="min-w-[32px]"
62
+ ariaLabel="Página anterior"
63
+ title="Ir a la página anterior"
64
+ >
65
+ <FaArrowLeft aria-hidden="true" />
66
+ </ITButton>
67
+
68
+ <ITText as="span" className="px-4 py-2 text-sm text-gray-700" aria-live="polite">
69
+ Página {pageIndex} de {totalPages}
70
+ </ITText>
71
+
72
+ <ITButton
73
+ size="small"
74
+ color="secondary"
75
+ onClick={() => onPageChange(pageIndex + 1)}
76
+ disabled={!hasNextPage}
77
+ className="min-w-[32px]"
78
+ ariaLabel="Página siguiente"
79
+ title="Ir a la página siguiente"
80
+ >
81
+ <FaArrowRight aria-hidden="true" />
82
+ </ITButton>
83
+ </div>
84
+ </div>
85
+ );
86
+ }