@conatel-sa/react-ui 0.2.3 → 0.2.5

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/dist/forms.mjs ADDED
@@ -0,0 +1,585 @@
1
+ // src/components/forms/PasswordInput.tsx
2
+ import { useId, useState } from "react";
3
+
4
+ // src/components/forms/passwordValidation.ts
5
+ var SPECIAL_REGEX = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?~`]/;
6
+ function validatePassword(password) {
7
+ return {
8
+ minLength: password.length >= 8,
9
+ hasLower: /[a-z]/.test(password),
10
+ hasUpper: /[A-Z]/.test(password),
11
+ hasDigit: /[0-9]/.test(password),
12
+ hasSpecial: SPECIAL_REGEX.test(password)
13
+ };
14
+ }
15
+ function isPasswordValid(checks) {
16
+ return checks.minLength && checks.hasLower && checks.hasUpper && checks.hasDigit && checks.hasSpecial;
17
+ }
18
+ function getPasswordValid(password) {
19
+ return isPasswordValid(validatePassword(password));
20
+ }
21
+ var PASSWORD_RULES = [
22
+ { key: "minLength", label: "M\xEDnimo 8 caracteres", ok: (c) => c.minLength },
23
+ { key: "hasLower", label: "Al menos una min\xFAscula", ok: (c) => c.hasLower },
24
+ { key: "hasUpper", label: "Al menos una may\xFAscula", ok: (c) => c.hasUpper },
25
+ { key: "hasDigit", label: "Al menos un n\xFAmero", ok: (c) => c.hasDigit },
26
+ { key: "hasSpecial", label: "Al menos un car\xE1cter especial (!@#$%^&* etc.)", ok: (c) => c.hasSpecial }
27
+ ];
28
+ var PASSWORD_HINT = "M\xEDn. 8 caracteres, may\xFAscula, min\xFAscula, n\xFAmero y un car\xE1cter especial.";
29
+
30
+ // src/components/forms/PasswordInput.tsx
31
+ import { jsx, jsxs } from "react/jsx-runtime";
32
+ function PasswordInput({
33
+ value,
34
+ onChange,
35
+ placeholder = "Contrase\xF1a",
36
+ label,
37
+ hint,
38
+ error,
39
+ disabled,
40
+ required,
41
+ ariaLabel,
42
+ showValidation = false,
43
+ id: idProp,
44
+ inputClassName = "form-input",
45
+ className = "form-group",
46
+ minLength = 8,
47
+ "data-testid": dataTestId,
48
+ leftIcon = "lock"
49
+ }) {
50
+ const [revealed, setRevealed] = useState(false);
51
+ const genId = useId();
52
+ const id = idProp ?? genId;
53
+ const checks = validatePassword(value);
54
+ const wrapClass = `${inputClassName} input-password-wrap`.trim();
55
+ return /* @__PURE__ */ jsxs("div", { className, children: [
56
+ label ? /* @__PURE__ */ jsxs("label", { className: "form-label-app", htmlFor: id, children: [
57
+ label,
58
+ required ? /* @__PURE__ */ jsxs("span", { className: "required-mark", "aria-hidden": true, children: [
59
+ " ",
60
+ "*"
61
+ ] }) : null
62
+ ] }) : null,
63
+ /* @__PURE__ */ jsxs(
64
+ "div",
65
+ {
66
+ className: wrapClass,
67
+ style: {
68
+ position: "relative",
69
+ display: "flex",
70
+ alignItems: "center",
71
+ padding: "0.75rem 2.5rem 0.75rem 3rem"
72
+ },
73
+ children: [
74
+ /* @__PURE__ */ jsx(
75
+ "span",
76
+ {
77
+ className: "material-symbols-outlined",
78
+ style: {
79
+ position: "absolute",
80
+ left: "1rem",
81
+ color: "var(--color-text-muted)",
82
+ fontSize: "1.25rem",
83
+ pointerEvents: "none"
84
+ },
85
+ "aria-hidden": true,
86
+ children: leftIcon
87
+ }
88
+ ),
89
+ /* @__PURE__ */ jsx(
90
+ "input",
91
+ {
92
+ type: revealed ? "text" : "password",
93
+ id,
94
+ "data-testid": dataTestId,
95
+ value,
96
+ disabled,
97
+ onChange: (e) => onChange(e.target.value),
98
+ placeholder,
99
+ title: PASSWORD_HINT,
100
+ minLength,
101
+ required,
102
+ "aria-label": ariaLabel ?? label ?? "Password",
103
+ style: {
104
+ border: "none",
105
+ background: "transparent",
106
+ flex: 1,
107
+ minWidth: 0,
108
+ padding: 0,
109
+ fontSize: "inherit",
110
+ outline: "none"
111
+ }
112
+ }
113
+ ),
114
+ /* @__PURE__ */ jsx(
115
+ "button",
116
+ {
117
+ type: "button",
118
+ onClick: () => setRevealed((v) => !v),
119
+ tabIndex: -1,
120
+ title: revealed ? "Ocultar contrase\xF1a" : "Mostrar contrase\xF1a",
121
+ disabled,
122
+ style: {
123
+ position: "absolute",
124
+ right: "0.5rem",
125
+ background: "none",
126
+ border: "none",
127
+ padding: "0.25rem",
128
+ cursor: disabled ? "not-allowed" : "pointer",
129
+ color: "var(--color-text-muted)"
130
+ },
131
+ children: /* @__PURE__ */ jsx("span", { className: "material-symbols-outlined", style: { fontSize: "1.25rem" }, children: revealed ? "visibility_off" : "visibility" })
132
+ }
133
+ )
134
+ ]
135
+ }
136
+ ),
137
+ showValidation ? /* @__PURE__ */ jsxs("p", { style: { marginTop: "0.5rem", fontSize: "0.75rem", color: "var(--color-text-muted)" }, children: [
138
+ "* ",
139
+ PASSWORD_HINT
140
+ ] }) : null,
141
+ showValidation && value.length > 0 ? /* @__PURE__ */ jsx(
142
+ "ul",
143
+ {
144
+ style: {
145
+ marginTop: "0.5rem",
146
+ paddingLeft: "1.25rem",
147
+ fontSize: "0.75rem",
148
+ color: "var(--color-text-muted)"
149
+ },
150
+ children: PASSWORD_RULES.map((rule) => /* @__PURE__ */ jsxs(
151
+ "li",
152
+ {
153
+ style: {
154
+ listStyle: "none",
155
+ marginLeft: "-1.25rem",
156
+ marginBottom: "0.25rem",
157
+ display: "flex",
158
+ alignItems: "center",
159
+ gap: "0.35rem"
160
+ },
161
+ children: [
162
+ /* @__PURE__ */ jsx(
163
+ "span",
164
+ {
165
+ style: {
166
+ color: rule.ok(checks) ? "var(--color-success)" : "var(--color-text-muted)",
167
+ fontWeight: rule.ok(checks) ? 600 : 400
168
+ },
169
+ children: rule.ok(checks) ? "\u2713" : "\u25CB"
170
+ }
171
+ ),
172
+ /* @__PURE__ */ jsx("span", { style: rule.ok(checks) ? { color: "var(--color-success)" } : void 0, children: rule.label })
173
+ ]
174
+ },
175
+ rule.key
176
+ ))
177
+ }
178
+ ) : null,
179
+ error ? /* @__PURE__ */ jsx("p", { role: "alert", style: { marginTop: "0.35rem", fontSize: "0.8125rem", color: "var(--color-error)" }, children: error }) : hint ? /* @__PURE__ */ jsx("p", { style: { marginTop: "0.35rem", fontSize: "0.8125rem", color: "var(--color-text-muted)" }, children: hint }) : null
180
+ ] });
181
+ }
182
+
183
+ // src/components/forms/MultiSelectDropdown.tsx
184
+ import { useEffect, useRef, useState as useState2 } from "react";
185
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
186
+ function MultiSelectDropdown({
187
+ label,
188
+ options,
189
+ value,
190
+ selected,
191
+ onChange,
192
+ placeholder = "Seleccionar\u2026",
193
+ emptyLabel,
194
+ disabled,
195
+ allowAllNone = true,
196
+ allLabel = "Todos",
197
+ noneLabel = "Ninguno",
198
+ style,
199
+ buttonClassName = "form-select",
200
+ className = "form-group"
201
+ }) {
202
+ const [open, setOpen] = useState2(false);
203
+ const rootRef = useRef(null);
204
+ const selectedValues = value ?? selected ?? [];
205
+ const effectivePlaceholder = emptyLabel ?? placeholder;
206
+ const isAllSelected = options.length > 0 && selectedValues.length === options.length;
207
+ const isNoneSelected = selectedValues.length === 0;
208
+ useEffect(() => {
209
+ if (!open) return;
210
+ const onDown = (e) => {
211
+ const el = rootRef.current;
212
+ if (!el) return;
213
+ const target = e.target;
214
+ if (target && el.contains(target)) return;
215
+ setOpen(false);
216
+ };
217
+ document.addEventListener("mousedown", onDown);
218
+ return () => document.removeEventListener("mousedown", onDown);
219
+ }, [open]);
220
+ const toggleValue = (v) => {
221
+ if (disabled) return;
222
+ if (selectedValues.includes(v)) onChange(selectedValues.filter((x) => x !== v));
223
+ else onChange([...selectedValues, v]);
224
+ };
225
+ const buttonLabel = selectedValues.length === 0 ? effectivePlaceholder : selectedValues.map((v) => options.find((o) => o.value === v)?.label ?? v).join(", ");
226
+ return /* @__PURE__ */ jsxs2("div", { ref: rootRef, className, style: { position: "relative", ...style ?? {} }, children: [
227
+ label ? /* @__PURE__ */ jsx2("label", { className: "form-label-dropdown", children: label }) : null,
228
+ /* @__PURE__ */ jsxs2(
229
+ "button",
230
+ {
231
+ type: "button",
232
+ className: buttonClassName,
233
+ onClick: () => {
234
+ if (disabled) return;
235
+ setOpen((o) => !o);
236
+ },
237
+ disabled,
238
+ style: {
239
+ width: "100%",
240
+ textAlign: "left",
241
+ overflow: "hidden",
242
+ textOverflow: "ellipsis",
243
+ whiteSpace: "nowrap",
244
+ display: "flex",
245
+ alignItems: "center",
246
+ justifyContent: "space-between",
247
+ gap: "0.5rem",
248
+ cursor: disabled ? "not-allowed" : "pointer",
249
+ opacity: disabled ? 0.6 : 1
250
+ },
251
+ "aria-haspopup": "listbox",
252
+ "aria-expanded": open,
253
+ children: [
254
+ /* @__PURE__ */ jsx2(
255
+ "span",
256
+ {
257
+ style: {
258
+ overflow: "hidden",
259
+ textOverflow: "ellipsis",
260
+ whiteSpace: "nowrap",
261
+ color: selectedValues.length === 0 ? "var(--color-text-muted)" : "var(--color-text-primary)"
262
+ },
263
+ children: buttonLabel
264
+ }
265
+ ),
266
+ /* @__PURE__ */ jsx2("span", { className: "material-symbols-outlined", style: { fontSize: "1rem", color: "var(--color-text-secondary)", flexShrink: 0 }, children: open ? "expand_less" : "expand_more" })
267
+ ]
268
+ }
269
+ ),
270
+ open && /* @__PURE__ */ jsxs2(
271
+ "div",
272
+ {
273
+ className: "form-dropdown-panel",
274
+ style: {
275
+ position: "absolute",
276
+ top: "100%",
277
+ left: 0,
278
+ minWidth: "100%",
279
+ maxHeight: "220px",
280
+ overflowY: "auto",
281
+ border: "1px solid var(--color-border)",
282
+ borderRadius: "var(--radius-md)",
283
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
284
+ marginTop: "2px"
285
+ },
286
+ children: [
287
+ allowAllNone && options.length > 0 && /* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "center", gap: "0.35rem", padding: "0.35rem 0.6rem", borderBottom: "1px solid var(--color-border)", fontSize: "0.75rem" }, children: [
288
+ /* @__PURE__ */ jsxs2(
289
+ "button",
290
+ {
291
+ type: "button",
292
+ onClick: () => onChange(options.map((o) => o.value)),
293
+ style: {
294
+ background: "none",
295
+ border: "none",
296
+ color: isAllSelected ? "var(--color-success)" : "var(--color-text-secondary)",
297
+ cursor: "pointer",
298
+ display: "inline-flex",
299
+ alignItems: "center",
300
+ gap: "0.2rem",
301
+ padding: 0,
302
+ fontWeight: isAllSelected ? 600 : 500
303
+ },
304
+ children: [
305
+ /* @__PURE__ */ jsx2("span", { className: "material-symbols-outlined", style: { fontSize: "0.9rem" }, children: "check" }),
306
+ /* @__PURE__ */ jsx2("span", { children: allLabel })
307
+ ]
308
+ }
309
+ ),
310
+ /* @__PURE__ */ jsx2("span", { style: { color: "var(--color-text-muted)" }, children: "|" }),
311
+ /* @__PURE__ */ jsxs2(
312
+ "button",
313
+ {
314
+ type: "button",
315
+ onClick: () => onChange([]),
316
+ style: {
317
+ background: "none",
318
+ border: "none",
319
+ color: isNoneSelected ? "var(--color-danger)" : "var(--color-text-secondary)",
320
+ cursor: "pointer",
321
+ display: "inline-flex",
322
+ alignItems: "center",
323
+ gap: "0.2rem",
324
+ padding: 0,
325
+ fontWeight: isNoneSelected ? 600 : 500
326
+ },
327
+ children: [
328
+ /* @__PURE__ */ jsx2("span", { className: "material-symbols-outlined", style: { fontSize: "0.9rem" }, children: "close" }),
329
+ /* @__PURE__ */ jsx2("span", { children: noneLabel })
330
+ ]
331
+ }
332
+ )
333
+ ] }),
334
+ options.length === 0 ? /* @__PURE__ */ jsx2("div", { style: { padding: "0.5rem 0.75rem", fontSize: "0.875rem", color: "var(--color-text-muted)" }, children: "Sin opciones" }) : null,
335
+ options.map((opt) => /* @__PURE__ */ jsxs2(
336
+ "label",
337
+ {
338
+ style: {
339
+ display: "flex",
340
+ alignItems: "center",
341
+ gap: "0.5rem",
342
+ padding: "0.4rem 0.75rem",
343
+ cursor: "pointer",
344
+ fontSize: "0.875rem",
345
+ color: selectedValues.includes(opt.value) ? "var(--color-text-primary)" : "var(--color-text-secondary)",
346
+ background: selectedValues.includes(opt.value) ? "var(--color-accent-bg)" : "transparent"
347
+ },
348
+ onMouseEnter: (e) => {
349
+ e.currentTarget.style.background = selectedValues.includes(opt.value) ? "var(--color-accent-bg)" : "var(--color-bg-hover)";
350
+ },
351
+ onMouseLeave: (e) => {
352
+ e.currentTarget.style.background = selectedValues.includes(opt.value) ? "var(--color-accent-bg)" : "transparent";
353
+ },
354
+ children: [
355
+ /* @__PURE__ */ jsx2(
356
+ "input",
357
+ {
358
+ type: "checkbox",
359
+ checked: selectedValues.includes(opt.value),
360
+ onChange: () => toggleValue(opt.value),
361
+ style: { width: "auto", margin: 0, accentColor: "var(--color-primary)" }
362
+ }
363
+ ),
364
+ opt.label
365
+ ]
366
+ },
367
+ opt.value
368
+ ))
369
+ ]
370
+ }
371
+ )
372
+ ] });
373
+ }
374
+
375
+ // src/components/forms/SingleSelectDropdown.tsx
376
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState3 } from "react";
377
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
378
+ function SingleSelectDropdown({
379
+ options,
380
+ value,
381
+ selected,
382
+ onChange,
383
+ placeholder = "Seleccionar",
384
+ emptyValue = "",
385
+ allowEmptyOption = true,
386
+ disabled,
387
+ label,
388
+ style,
389
+ buttonClassName = "form-select",
390
+ dataTestId,
391
+ title,
392
+ ariaLabel,
393
+ buttonStyle,
394
+ className = "form-group"
395
+ }) {
396
+ const [open, setOpen] = useState3(false);
397
+ const rootRef = useRef2(null);
398
+ const currentValue = value ?? selected ?? emptyValue;
399
+ const selectedOption = options.find((o) => o.value === currentValue);
400
+ const buttonLabel = selectedOption?.label ?? placeholder;
401
+ const renderedOptions = allowEmptyOption ? [{ value: emptyValue, label: placeholder }, ...options] : options;
402
+ useEffect2(() => {
403
+ if (!open) return;
404
+ const onDown = (e) => {
405
+ const el = rootRef.current;
406
+ if (!el) return;
407
+ const target = e.target;
408
+ if (target && el.contains(target)) return;
409
+ setOpen(false);
410
+ };
411
+ document.addEventListener("mousedown", onDown);
412
+ return () => document.removeEventListener("mousedown", onDown);
413
+ }, [open]);
414
+ return /* @__PURE__ */ jsxs3("div", { ref: rootRef, className, style: { position: "relative", ...style ?? {} }, children: [
415
+ label ? /* @__PURE__ */ jsx3("label", { className: "form-label-dropdown", children: label }) : null,
416
+ /* @__PURE__ */ jsxs3(
417
+ "button",
418
+ {
419
+ type: "button",
420
+ className: buttonClassName,
421
+ "data-testid": dataTestId,
422
+ title,
423
+ "aria-label": ariaLabel,
424
+ onClick: () => {
425
+ if (disabled) return;
426
+ setOpen((o) => !o);
427
+ },
428
+ disabled,
429
+ style: {
430
+ width: "100%",
431
+ textAlign: "left",
432
+ overflow: "hidden",
433
+ textOverflow: "ellipsis",
434
+ whiteSpace: "nowrap",
435
+ display: "flex",
436
+ alignItems: "center",
437
+ justifyContent: "space-between",
438
+ gap: "0.5rem",
439
+ cursor: disabled ? "not-allowed" : "pointer",
440
+ opacity: disabled ? 0.6 : 1,
441
+ ...buttonStyle ?? {}
442
+ },
443
+ "aria-haspopup": "listbox",
444
+ "aria-expanded": open,
445
+ children: [
446
+ /* @__PURE__ */ jsx3(
447
+ "span",
448
+ {
449
+ style: {
450
+ overflow: "hidden",
451
+ textOverflow: "ellipsis",
452
+ whiteSpace: "nowrap",
453
+ color: currentValue === emptyValue ? "var(--color-text-muted)" : "var(--color-text-primary)"
454
+ },
455
+ children: buttonLabel
456
+ }
457
+ ),
458
+ /* @__PURE__ */ jsx3("span", { className: "material-symbols-outlined", style: { fontSize: "1rem", color: "var(--color-text-secondary)", flexShrink: 0 }, children: open ? "expand_less" : "expand_more" })
459
+ ]
460
+ }
461
+ ),
462
+ open ? /* @__PURE__ */ jsxs3(
463
+ "div",
464
+ {
465
+ className: "form-dropdown-panel",
466
+ style: {
467
+ position: "absolute",
468
+ top: "100%",
469
+ left: 0,
470
+ minWidth: "100%",
471
+ maxHeight: "220px",
472
+ overflowY: "auto",
473
+ border: "1px solid var(--color-border)",
474
+ borderRadius: "var(--radius-md)",
475
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
476
+ marginTop: "2px"
477
+ },
478
+ role: "listbox",
479
+ children: [
480
+ renderedOptions.length === 0 ? /* @__PURE__ */ jsx3("div", { style: { padding: "0.5rem 0.75rem", fontSize: "0.875rem", color: "var(--color-text-muted)" }, children: "Sin opciones" }) : null,
481
+ renderedOptions.map((opt) => {
482
+ const isSelected = opt.value === currentValue;
483
+ return /* @__PURE__ */ jsxs3(
484
+ "div",
485
+ {
486
+ role: "option",
487
+ "aria-selected": isSelected,
488
+ onClick: () => {
489
+ onChange(opt.value);
490
+ setOpen(false);
491
+ },
492
+ onMouseEnter: (e) => {
493
+ e.currentTarget.style.background = isSelected ? "var(--color-accent-bg)" : "var(--color-bg-hover)";
494
+ },
495
+ onMouseLeave: (e) => {
496
+ e.currentTarget.style.background = isSelected ? "var(--color-accent-bg)" : "transparent";
497
+ },
498
+ style: {
499
+ display: "flex",
500
+ alignItems: "center",
501
+ gap: "0.5rem",
502
+ padding: "0.4rem 0.75rem",
503
+ cursor: "pointer",
504
+ fontSize: "0.875rem",
505
+ color: isSelected ? "var(--color-text-primary)" : "var(--color-text-secondary)",
506
+ background: isSelected ? "var(--color-accent-bg)" : "transparent",
507
+ userSelect: "none"
508
+ },
509
+ children: [
510
+ isSelected ? /* @__PURE__ */ jsx3("span", { className: "material-symbols-outlined", style: { fontSize: "0.95rem" }, children: "check" }) : null,
511
+ opt.label
512
+ ]
513
+ },
514
+ opt.value
515
+ );
516
+ })
517
+ ]
518
+ }
519
+ ) : null
520
+ ] });
521
+ }
522
+
523
+ // src/components/forms/EmailInput.tsx
524
+ import { useId as useId2 } from "react";
525
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
526
+ function EmailInput({
527
+ value,
528
+ onChange,
529
+ placeholder = "correo@ejemplo.com",
530
+ label = "Correo electr\xF3nico",
531
+ hint,
532
+ error,
533
+ disabled,
534
+ required,
535
+ ariaLabel,
536
+ id: idProp,
537
+ leftIcon = "mail",
538
+ className = "form-group",
539
+ inputClassName = "form-input",
540
+ autoComplete = "email",
541
+ "data-testid": dataTestId
542
+ }) {
543
+ const genId = useId2();
544
+ const id = idProp ?? genId;
545
+ return /* @__PURE__ */ jsxs4("div", { className, children: [
546
+ label ? /* @__PURE__ */ jsxs4("label", { className: "form-label-app", htmlFor: id, children: [
547
+ label,
548
+ required ? /* @__PURE__ */ jsxs4("span", { className: "required-mark", "aria-hidden": true, children: [
549
+ " ",
550
+ "*"
551
+ ] }) : null
552
+ ] }) : null,
553
+ /* @__PURE__ */ jsxs4("div", { className: "input-with-icon", children: [
554
+ /* @__PURE__ */ jsx4("span", { className: "material-symbols-outlined", "aria-hidden": true, children: leftIcon }),
555
+ /* @__PURE__ */ jsx4(
556
+ "input",
557
+ {
558
+ id,
559
+ "data-testid": dataTestId,
560
+ type: "email",
561
+ value,
562
+ onChange: (e) => onChange(e.target.value),
563
+ placeholder,
564
+ className: inputClassName,
565
+ required,
566
+ disabled,
567
+ autoComplete,
568
+ "aria-label": ariaLabel ?? label ?? "Email"
569
+ }
570
+ )
571
+ ] }),
572
+ error ? /* @__PURE__ */ jsx4("p", { role: "alert", style: { marginTop: "0.35rem", fontSize: "0.8125rem", color: "var(--color-error)" }, children: error }) : hint ? /* @__PURE__ */ jsx4("p", { style: { marginTop: "0.35rem", fontSize: "0.8125rem", color: "var(--color-text-muted)" }, children: hint }) : null
573
+ ] });
574
+ }
575
+ export {
576
+ EmailInput,
577
+ MultiSelectDropdown,
578
+ PASSWORD_HINT,
579
+ PASSWORD_RULES,
580
+ PasswordInput,
581
+ SingleSelectDropdown,
582
+ getPasswordValid,
583
+ isPasswordValid,
584
+ validatePassword
585
+ };