@zauru-sdk/components 6.11.0 → 6.12.0

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/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [6.12.0](https://github.com/intuitiva/zauru-typescript-sdk/compare/@zauru-sdk/components@6.11.1...@zauru-sdk/components@6.12.0) (2026-10-07)
7
+
8
+
9
+ ### Features
10
+
11
+ * agregar submenús anidados al dropdown de Button ([8bf8c5e](https://github.com/intuitiva/zauru-typescript-sdk/commit/8bf8c5ecb80d03cb10f806d912f148084576d655))
12
+
13
+
14
+
15
+
16
+
17
+ ## [6.11.1](https://github.com/intuitiva/zauru-typescript-sdk/compare/@zauru-sdk/components@6.11.0...@zauru-sdk/components@6.11.1) (2026-10-07)
18
+
19
+ **Note:** Version bump only for package @zauru-sdk/components
20
+
21
+
22
+
23
+
24
+
6
25
  # [6.11.0](https://github.com/intuitiva/zauru-typescript-sdk/compare/@zauru-sdk/components@6.10.0...@zauru-sdk/components@6.11.0) (2026-10-05)
7
26
 
8
27
 
@@ -1,17 +1,19 @@
1
+ import { type MouseEvent as ReactMouseEvent, type ReactNode } from "react";
1
2
  export type DropdownOption = {
2
3
  label: string;
3
4
  value: string;
4
- onClick: () => void;
5
+ onClick?: () => void;
6
+ children?: DropdownOption[];
5
7
  };
6
8
  type Props = {
7
9
  type?: "reset" | "button" | "submit" | undefined;
8
10
  title?: string;
9
11
  name?: string;
10
- onClickSave?: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
12
+ onClickSave?: (e: ReactMouseEvent<HTMLButtonElement>) => void;
11
13
  loading?: boolean;
12
14
  loadingText?: string;
13
15
  selectedColor?: "indigo" | "green" | "red" | "yellow" | "gray";
14
- children?: React.ReactNode;
16
+ children?: ReactNode;
15
17
  className?: string;
16
18
  disabled?: boolean;
17
19
  enableFormErrorsValidation?: boolean;
@@ -1,6 +1,75 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useFormContext } from "react-hook-form";
3
- import { useState, useRef, useEffect } from "react";
3
+ import { useEffect, useRef, useState, } from "react";
4
+ const SUBMENU_OPEN_DELAY_MS = 150;
5
+ const SUBMENU_CLOSE_DELAY_MS = 200;
6
+ const prefersReducedMotion = () => typeof window !== "undefined" &&
7
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches;
8
+ const DropdownMenuItem = ({ option, onLeafClick }) => {
9
+ const hasChildren = Boolean(option.children?.length);
10
+ const [isSubmenuOpen, setIsSubmenuOpen] = useState(false);
11
+ const openTimerRef = useRef(null);
12
+ const closeTimerRef = useRef(null);
13
+ const clearTimers = () => {
14
+ if (openTimerRef.current != null) {
15
+ window.clearTimeout(openTimerRef.current);
16
+ openTimerRef.current = null;
17
+ }
18
+ if (closeTimerRef.current != null) {
19
+ window.clearTimeout(closeTimerRef.current);
20
+ closeTimerRef.current = null;
21
+ }
22
+ };
23
+ useEffect(() => {
24
+ return () => {
25
+ clearTimers();
26
+ };
27
+ }, []);
28
+ const openSubmenu = (immediate = false) => {
29
+ if (!hasChildren)
30
+ return;
31
+ clearTimers();
32
+ if (immediate || prefersReducedMotion()) {
33
+ setIsSubmenuOpen(true);
34
+ return;
35
+ }
36
+ openTimerRef.current = window.setTimeout(() => {
37
+ setIsSubmenuOpen(true);
38
+ }, SUBMENU_OPEN_DELAY_MS);
39
+ };
40
+ const closeSubmenu = (immediate = false) => {
41
+ clearTimers();
42
+ if (immediate || prefersReducedMotion()) {
43
+ setIsSubmenuOpen(false);
44
+ return;
45
+ }
46
+ closeTimerRef.current = window.setTimeout(() => {
47
+ setIsSubmenuOpen(false);
48
+ }, SUBMENU_CLOSE_DELAY_MS);
49
+ };
50
+ const handleClick = () => {
51
+ if (hasChildren) {
52
+ setIsSubmenuOpen((open) => !open);
53
+ return;
54
+ }
55
+ option.onClick?.();
56
+ onLeafClick();
57
+ };
58
+ const handleKeyDown = (event) => {
59
+ if (!hasChildren)
60
+ return;
61
+ if (event.key === "ArrowRight" || event.key === "Enter") {
62
+ event.preventDefault();
63
+ openSubmenu(true);
64
+ }
65
+ if (event.key === "ArrowLeft" || event.key === "Escape") {
66
+ event.preventDefault();
67
+ closeSubmenu(true);
68
+ }
69
+ };
70
+ const itemClassName = "flex w-full cursor-pointer items-center justify-between gap-2 px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:ring-2 focus:ring-indigo-500 focus:ring-inset motion-reduce:transition-none";
71
+ return (_jsxs("div", { className: "relative", onMouseEnter: () => openSubmenu(), onMouseLeave: () => closeSubmenu(), children: [_jsxs("button", { type: "button", role: "menuitem", "aria-haspopup": hasChildren ? "menu" : undefined, "aria-expanded": hasChildren ? isSubmenuOpen : undefined, onClick: handleClick, onKeyDown: handleKeyDown, className: itemClassName, children: [_jsx("span", { children: option.label }), hasChildren ? (_jsx("svg", { "aria-hidden": "true", className: "h-4 w-4 shrink-0 text-gray-500", xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 20 20", fill: "currentColor", children: _jsx("path", { fillRule: "evenodd", d: "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", clipRule: "evenodd" }) })) : null] }), hasChildren && isSubmenuOpen ? (_jsx("div", { role: "menu", className: "absolute right-full top-0 z-20 mr-1 w-56 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none", children: option.children?.map((child) => (_jsx(DropdownMenuItem, { option: child, onLeafClick: onLeafClick }, child.value))) })) : null] }));
72
+ };
4
73
  export const Button = (props) => {
5
74
  const { type = "submit", loading = false, loadingText = "Guardando...", title = "Guardar", name = "save", onClickSave, selectedColor = "indigo", children, className = "", disabled = false, enableFormErrorsValidation = false, enableFormErrorsDescriptions = false, dropdownOptions = [], dropdownTitle, } = props;
6
75
  const [isDropdownOpen, setIsDropdownOpen] = useState(false);
@@ -69,9 +138,16 @@ export const Button = (props) => {
69
138
  setIsDropdownOpen(false);
70
139
  }
71
140
  };
141
+ const handleEscape = (event) => {
142
+ if (event.key === "Escape") {
143
+ setIsDropdownOpen(false);
144
+ }
145
+ };
72
146
  document.addEventListener("mousedown", handleClickOutside);
147
+ document.addEventListener("keydown", handleEscape);
73
148
  return () => {
74
149
  document.removeEventListener("mousedown", handleClickOutside);
150
+ document.removeEventListener("keydown", handleEscape);
75
151
  };
76
152
  }, []);
77
153
  const isButtonDisabled = loading || disabled || (enableFormErrorsValidation && formHasErrors);
@@ -89,10 +165,7 @@ export const Button = (props) => {
89
165
  ? " cursor-progress"
90
166
  : `${isButtonDisabled ? " cursor-not-allowed" : `cursor-pointer hover:${color.bg700}`}`} inline-flex justify-center items-center rounded-md border border-transparent ${color.bg600} py-2 px-4 text-sm font-medium text-white shadow-sm focus:outline-none focus:ring-2 focus:${color.ring500} focus:ring-offset-2 ${className}`, children: [loading
91
167
  ? children ?? loadingText
92
- : children ?? dropdownTitle ?? title, _jsx("svg", { className: `ml-2 -mr-1 h-4 w-4 transition-transform ${isDropdownOpen ? "rotate-180" : ""}`, xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 20 20", fill: "currentColor", children: _jsx("path", { fillRule: "evenodd", d: "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z", clipRule: "evenodd" }) })] }) }), isDropdownOpen && !isButtonDisabled ? (_jsx("div", { role: "menu", className: "absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none", children: _jsx("div", { className: "py-1", children: dropdownOptions.map((option) => (_jsx("button", { type: "button", role: "menuitem", onClick: () => {
93
- option.onClick();
94
- setIsDropdownOpen(false);
95
- }, className: "block w-full cursor-pointer px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 text-left", children: option.label }, option.value))) }) })) : null] }));
168
+ : children ?? dropdownTitle ?? title, _jsx("svg", { className: `ml-2 -mr-1 h-4 w-4 transition-transform motion-reduce:transition-none ${isDropdownOpen ? "rotate-180" : ""}`, xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { fillRule: "evenodd", d: "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z", clipRule: "evenodd" }) })] }) }), isDropdownOpen && !isButtonDisabled ? (_jsx("div", { role: "menu", className: "absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none", children: _jsx("div", { className: "py-1", children: dropdownOptions.map((option) => (_jsx(DropdownMenuItem, { option: option, onLeafClick: () => setIsDropdownOpen(false) }, option.value))) }) })) : null] }));
96
169
  return (_jsxs(_Fragment, { children: [(enableFormErrorsValidation && formHasErrors && errorMessage) ||
97
170
  (enableFormErrorsDescriptions && errorMessage) ? (_jsx("div", { className: "flex flex-col items-end mb-2", children: _jsx("div", { className: "p-2 bg-red-100 border border-red-400 text-red-700 rounded-md shadow-sm", children: _jsx("p", { className: "text-sm", children: errorMessage }) }) })) : null, dropdownContent] }));
98
171
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zauru-sdk/components",
3
- "version": "6.11.0",
3
+ "version": "6.12.0",
4
4
  "description": "Componentes reutilizables en las WebApps de Zauru.",
5
5
  "main": "./dist/esm/index.js",
6
6
  "module": "./dist/esm/index.js",
@@ -44,12 +44,12 @@
44
44
  "@reduxjs/toolkit": "^2.2.1",
45
45
  "@remix-run/node": "^2.8.1",
46
46
  "@remix-run/react": "^2.8.1",
47
- "@zauru-sdk/common": "^6.8.5",
48
- "@zauru-sdk/hooks": "^6.3.18",
47
+ "@zauru-sdk/common": "^6.8.6",
48
+ "@zauru-sdk/hooks": "^6.3.19",
49
49
  "@zauru-sdk/icons": "^6.1.0",
50
- "@zauru-sdk/redux": "^6.5.16",
51
- "@zauru-sdk/types": "^6.23.0",
52
- "@zauru-sdk/utils": "^6.22.0",
50
+ "@zauru-sdk/redux": "^6.5.17",
51
+ "@zauru-sdk/types": "^6.24.0",
52
+ "@zauru-sdk/utils": "^6.22.1",
53
53
  "browser-image-compression": "^2.0.2",
54
54
  "framer-motion": "^12.0.0",
55
55
  "jsonwebtoken": "^9.0.2",
@@ -62,5 +62,5 @@
62
62
  "styled-components": "^5.3.5",
63
63
  "zod": "^3.23.8"
64
64
  },
65
- "gitHead": "885c1d44afb4c5717ae823664b37ac48b30283db"
65
+ "gitHead": "9ab463da344ee7b835fc9e1367d75d70f357ac1a"
66
66
  }
@@ -1,23 +1,163 @@
1
1
  import type { ColorInterface } from "../NavBar/NavBar.types.js";
2
2
  import { useFormContext } from "react-hook-form";
3
- import { useState, useRef, useEffect } from "react";
3
+ import {
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
+ type KeyboardEvent as ReactKeyboardEvent,
8
+ type MouseEvent as ReactMouseEvent,
9
+ type ReactNode,
10
+ } from "react";
4
11
 
5
12
  export type DropdownOption = {
6
13
  label: string;
7
14
  value: string;
8
- onClick: () => void;
15
+ onClick?: () => void;
16
+ children?: DropdownOption[];
17
+ };
18
+
19
+ const SUBMENU_OPEN_DELAY_MS = 150;
20
+ const SUBMENU_CLOSE_DELAY_MS = 200;
21
+
22
+ const prefersReducedMotion = () =>
23
+ typeof window !== "undefined" &&
24
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches;
25
+
26
+ type DropdownMenuItemProps = {
27
+ option: DropdownOption;
28
+ onLeafClick: () => void;
29
+ };
30
+
31
+ const DropdownMenuItem = ({ option, onLeafClick }: DropdownMenuItemProps) => {
32
+ const hasChildren = Boolean(option.children?.length);
33
+ const [isSubmenuOpen, setIsSubmenuOpen] = useState(false);
34
+ const openTimerRef = useRef<number | null>(null);
35
+ const closeTimerRef = useRef<number | null>(null);
36
+
37
+ const clearTimers = () => {
38
+ if (openTimerRef.current != null) {
39
+ window.clearTimeout(openTimerRef.current);
40
+ openTimerRef.current = null;
41
+ }
42
+ if (closeTimerRef.current != null) {
43
+ window.clearTimeout(closeTimerRef.current);
44
+ closeTimerRef.current = null;
45
+ }
46
+ };
47
+
48
+ useEffect(() => {
49
+ return () => {
50
+ clearTimers();
51
+ };
52
+ }, []);
53
+
54
+ const openSubmenu = (immediate = false) => {
55
+ if (!hasChildren) return;
56
+ clearTimers();
57
+ if (immediate || prefersReducedMotion()) {
58
+ setIsSubmenuOpen(true);
59
+ return;
60
+ }
61
+ openTimerRef.current = window.setTimeout(() => {
62
+ setIsSubmenuOpen(true);
63
+ }, SUBMENU_OPEN_DELAY_MS);
64
+ };
65
+
66
+ const closeSubmenu = (immediate = false) => {
67
+ clearTimers();
68
+ if (immediate || prefersReducedMotion()) {
69
+ setIsSubmenuOpen(false);
70
+ return;
71
+ }
72
+ closeTimerRef.current = window.setTimeout(() => {
73
+ setIsSubmenuOpen(false);
74
+ }, SUBMENU_CLOSE_DELAY_MS);
75
+ };
76
+
77
+ const handleClick = () => {
78
+ if (hasChildren) {
79
+ setIsSubmenuOpen((open) => !open);
80
+ return;
81
+ }
82
+ option.onClick?.();
83
+ onLeafClick();
84
+ };
85
+
86
+ const handleKeyDown = (event: ReactKeyboardEvent<HTMLButtonElement>) => {
87
+ if (!hasChildren) return;
88
+ if (event.key === "ArrowRight" || event.key === "Enter") {
89
+ event.preventDefault();
90
+ openSubmenu(true);
91
+ }
92
+ if (event.key === "ArrowLeft" || event.key === "Escape") {
93
+ event.preventDefault();
94
+ closeSubmenu(true);
95
+ }
96
+ };
97
+
98
+ const itemClassName =
99
+ "flex w-full cursor-pointer items-center justify-between gap-2 px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:ring-2 focus:ring-indigo-500 focus:ring-inset motion-reduce:transition-none";
100
+
101
+ return (
102
+ <div
103
+ className="relative"
104
+ onMouseEnter={() => openSubmenu()}
105
+ onMouseLeave={() => closeSubmenu()}
106
+ >
107
+ <button
108
+ type="button"
109
+ role="menuitem"
110
+ aria-haspopup={hasChildren ? "menu" : undefined}
111
+ aria-expanded={hasChildren ? isSubmenuOpen : undefined}
112
+ onClick={handleClick}
113
+ onKeyDown={handleKeyDown}
114
+ className={itemClassName}
115
+ >
116
+ <span>{option.label}</span>
117
+ {hasChildren ? (
118
+ <svg
119
+ aria-hidden="true"
120
+ className="h-4 w-4 shrink-0 text-gray-500"
121
+ xmlns="http://www.w3.org/2000/svg"
122
+ viewBox="0 0 20 20"
123
+ fill="currentColor"
124
+ >
125
+ <path
126
+ fillRule="evenodd"
127
+ d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
128
+ clipRule="evenodd"
129
+ />
130
+ </svg>
131
+ ) : null}
132
+ </button>
133
+ {hasChildren && isSubmenuOpen ? (
134
+ <div
135
+ role="menu"
136
+ className="absolute right-full top-0 z-20 mr-1 w-56 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
137
+ >
138
+ {option.children?.map((child) => (
139
+ <DropdownMenuItem
140
+ key={child.value}
141
+ option={child}
142
+ onLeafClick={onLeafClick}
143
+ />
144
+ ))}
145
+ </div>
146
+ ) : null}
147
+ </div>
148
+ );
9
149
  };
10
150
 
11
151
  type Props = {
12
152
  type?: "reset" | "button" | "submit" | undefined;
13
153
  title?: string;
14
154
  name?: string;
15
- onClickSave?: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
155
+ onClickSave?: (e: ReactMouseEvent<HTMLButtonElement>) => void;
16
156
  //Cargando...
17
157
  loading?: boolean;
18
158
  loadingText?: string;
19
159
  selectedColor?: "indigo" | "green" | "red" | "yellow" | "gray";
20
- children?: React.ReactNode;
160
+ children?: ReactNode;
21
161
  className?: string;
22
162
  disabled?: boolean;
23
163
  enableFormErrorsValidation?: boolean;
@@ -119,9 +259,17 @@ export const Button = (props: Props) => {
119
259
  }
120
260
  };
121
261
 
262
+ const handleEscape = (event: KeyboardEvent) => {
263
+ if (event.key === "Escape") {
264
+ setIsDropdownOpen(false);
265
+ }
266
+ };
267
+
122
268
  document.addEventListener("mousedown", handleClickOutside);
269
+ document.addEventListener("keydown", handleEscape);
123
270
  return () => {
124
271
  document.removeEventListener("mousedown", handleClickOutside);
272
+ document.removeEventListener("keydown", handleEscape);
125
273
  };
126
274
  }, []);
127
275
 
@@ -191,12 +339,13 @@ export const Button = (props: Props) => {
191
339
  ? children ?? loadingText
192
340
  : children ?? dropdownTitle ?? title}
193
341
  <svg
194
- className={`ml-2 -mr-1 h-4 w-4 transition-transform ${
342
+ className={`ml-2 -mr-1 h-4 w-4 transition-transform motion-reduce:transition-none ${
195
343
  isDropdownOpen ? "rotate-180" : ""
196
344
  }`}
197
345
  xmlns="http://www.w3.org/2000/svg"
198
346
  viewBox="0 0 20 20"
199
347
  fill="currentColor"
348
+ aria-hidden="true"
200
349
  >
201
350
  <path
202
351
  fillRule="evenodd"
@@ -214,18 +363,11 @@ export const Button = (props: Props) => {
214
363
  >
215
364
  <div className="py-1">
216
365
  {dropdownOptions.map((option) => (
217
- <button
366
+ <DropdownMenuItem
218
367
  key={option.value}
219
- type="button"
220
- role="menuitem"
221
- onClick={() => {
222
- option.onClick();
223
- setIsDropdownOpen(false);
224
- }}
225
- className="block w-full cursor-pointer px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 text-left"
226
- >
227
- {option.label}
228
- </button>
368
+ option={option}
369
+ onLeafClick={() => setIsDropdownOpen(false)}
370
+ />
229
371
  ))}
230
372
  </div>
231
373
  </div>