@zauru-sdk/components 6.18.1 → 6.19.1
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 +27 -0
- package/build/assets/Alerts.stories-Dw0Ct9vA.js +1 -0
- package/build/assets/Button-YPU2MaHD.js +1 -0
- package/build/assets/Button.stories-CP6jmzaX.js +1 -0
- package/build/assets/Card.stories-DbFqLafy.js +1 -0
- package/build/assets/Container.stories-BhnM3_6Y.js +1 -0
- package/build/assets/Form.stories-BNB6SmSx.js +1 -0
- package/build/assets/Icons-BxLN_JWC.js +10 -0
- package/build/assets/Labels.stories-Fah6wLSi.js +1 -0
- package/build/assets/Matrix.stories-Dc3-VDms.js +1 -0
- package/build/assets/Modal.stories-D7kIP3bJ.js +1 -0
- package/build/assets/NavBar.stories-DuIgKZkc.js +1 -0
- package/build/assets/SidePanel.stories-Xsm8Md4K.js +1 -0
- package/build/assets/Tab.stories-RKI0m-pt.js +365 -0
- package/build/assets/Table.stories-CGj0WWPk.js +416 -0
- package/build/assets/Titles.stories-CPIRDaJs.js +1 -0
- package/build/assets/Tooltip.stories-D8M6F7St.js +1 -0
- package/build/assets/activestorage.esm-ByWQ3T27.js +1 -0
- package/build/assets/axe-sLlwVNub.js +30 -0
- package/build/assets/components-FzdPzEyL.js +46 -0
- package/build/assets/favicon-gjFwcFbF.svg +22 -0
- package/build/assets/index-CraKZ-Fr.css +1 -0
- package/build/assets/index-DUSjZECW.js +152 -0
- package/build/assets/index-DonOhaBM.js +1 -0
- package/build/assets/manifest-B-VxemCC.webmanifest +7 -0
- package/build/assets/proxy-BETbFm71.js +9 -0
- package/build/assets/touch-icon-ByyUZD7t.png +0 -0
- package/build/index.html +28 -0
- package/build/meta.json +416 -0
- package/dist/Matrix/CheckMatrixCards.d.ts +27 -0
- package/dist/Matrix/ResponsiveCheckMatrix.d.ts +7 -0
- package/dist/Matrix/index.d.ts +2 -0
- package/dist/SidePanel/index.d.ts +4 -2
- package/dist/esm/Matrix/CheckMatrixCards.js +26 -0
- package/dist/esm/Matrix/ResponsiveCheckMatrix.js +11 -0
- package/dist/esm/Matrix/index.js +2 -0
- package/dist/esm/SidePanel/index.js +53 -10
- package/dist/esm/index.js +1 -0
- package/dist/index.d.ts +1 -0
- package/package.json +9 -9
- package/src/Matrix/CheckMatrixCards.tsx +138 -0
- package/src/Matrix/ResponsiveCheckMatrix.tsx +32 -0
- package/src/Matrix/index.ts +2 -0
- package/src/SidePanel/index.tsx +101 -12
- package/src/index.ts +1 -0
- package/stories/Matrix.stories.tsx +75 -0
- package/stories/SidePanel.stories.tsx +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zauru-sdk/components",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.19.1",
|
|
4
4
|
"description": "Componentes reutilizables en las WebApps de Zauru.",
|
|
5
5
|
"main": "./dist/esm/index.js",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"@tailwindcss/forms": "^0.5.7",
|
|
28
28
|
"@types/jsonwebtoken": "^9.0.2",
|
|
29
29
|
"@types/rails__activestorage": "^7.1.1",
|
|
30
|
-
"@types/react": "^
|
|
31
|
-
"@types/react-dom": "^
|
|
30
|
+
"@types/react": "^18.2.20",
|
|
31
|
+
"@types/react-dom": "^18.2.7",
|
|
32
32
|
"@types/styled-components": "^5.1.34",
|
|
33
33
|
"autoprefixer": "^10.6.0",
|
|
34
34
|
"postcss": "^8.4.35",
|
|
@@ -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.14.
|
|
48
|
-
"@zauru-sdk/hooks": "^6.4.
|
|
47
|
+
"@zauru-sdk/common": "^6.14.2",
|
|
48
|
+
"@zauru-sdk/hooks": "^6.4.4",
|
|
49
49
|
"@zauru-sdk/icons": "^6.2.0",
|
|
50
|
-
"@zauru-sdk/redux": "^6.8.
|
|
51
|
-
"@zauru-sdk/types": "^6.30.
|
|
52
|
-
"@zauru-sdk/utils": "^6.30.
|
|
50
|
+
"@zauru-sdk/redux": "^6.8.4",
|
|
51
|
+
"@zauru-sdk/types": "^6.30.2",
|
|
52
|
+
"@zauru-sdk/utils": "^6.30.3",
|
|
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": "
|
|
65
|
+
"gitHead": "08f14cc3a87fabe1b2dfe678b9fdeb32ed4aaaef"
|
|
66
66
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import React, { ReactNode, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export type CheckMatrixColumn = {
|
|
4
|
+
key: string;
|
|
5
|
+
label: ReactNode;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type CheckMatrixRow = {
|
|
9
|
+
key: string;
|
|
10
|
+
label: ReactNode;
|
|
11
|
+
description?: ReactNode;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export interface CheckMatrixCardsProps {
|
|
15
|
+
rows: CheckMatrixRow[];
|
|
16
|
+
columns: CheckMatrixColumn[];
|
|
17
|
+
isChecked: (rowKey: string, columnKey: string) => boolean;
|
|
18
|
+
isPending?: (rowKey: string, columnKey: string) => boolean;
|
|
19
|
+
onToggle: (rowKey: string, columnKey: string, checked: boolean) => void;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
renderRowAction?: (row: CheckMatrixRow) => ReactNode;
|
|
22
|
+
checkedLabel?: string;
|
|
23
|
+
uncheckedLabel?: string;
|
|
24
|
+
pendingLabel?: string;
|
|
25
|
+
emptyText?: string;
|
|
26
|
+
pageSize?: number;
|
|
27
|
+
showMoreLabel?: string;
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const CheckMatrixCards = ({
|
|
32
|
+
rows,
|
|
33
|
+
columns,
|
|
34
|
+
isChecked,
|
|
35
|
+
isPending = () => false,
|
|
36
|
+
onToggle,
|
|
37
|
+
disabled = false,
|
|
38
|
+
renderRowAction,
|
|
39
|
+
checkedLabel = "Marcado",
|
|
40
|
+
uncheckedLabel = "Sin marcar",
|
|
41
|
+
pendingLabel = "Guardando…",
|
|
42
|
+
emptyText = "No hay filas para mostrar.",
|
|
43
|
+
pageSize = 20,
|
|
44
|
+
showMoreLabel = "Mostrar más",
|
|
45
|
+
className = "",
|
|
46
|
+
}: CheckMatrixCardsProps) => {
|
|
47
|
+
const safePageSize = Math.max(1, pageSize);
|
|
48
|
+
const [visibleCount, setVisibleCount] = useState(safePageSize);
|
|
49
|
+
const rowKeys = rows.map((row) => row.key).join("\u0000");
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
setVisibleCount(safePageSize);
|
|
53
|
+
}, [rowKeys, safePageSize]);
|
|
54
|
+
|
|
55
|
+
if (rows.length === 0) {
|
|
56
|
+
return <p className={`p-4 text-sm text-gray-600 ${className}`}>{emptyText}</p>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const visibleRows = rows.slice(0, visibleCount);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className={`space-y-3 ${className}`}>
|
|
63
|
+
{visibleRows.map((row) => (
|
|
64
|
+
<fieldset
|
|
65
|
+
key={row.key}
|
|
66
|
+
className="min-w-0 rounded-lg border border-gray-200 bg-white p-3 shadow-sm"
|
|
67
|
+
>
|
|
68
|
+
<div className="mb-3 flex min-w-0 items-start justify-between gap-3">
|
|
69
|
+
<legend className="min-w-0 flex-1 px-0 text-sm font-semibold text-gray-900">
|
|
70
|
+
<span className="block break-words">{row.label}</span>
|
|
71
|
+
{row.description ? (
|
|
72
|
+
<span className="mt-0.5 block break-words text-xs font-normal text-gray-600">
|
|
73
|
+
{row.description}
|
|
74
|
+
</span>
|
|
75
|
+
) : null}
|
|
76
|
+
</legend>
|
|
77
|
+
{renderRowAction ? (
|
|
78
|
+
<div className="shrink-0">{renderRowAction(row)}</div>
|
|
79
|
+
) : null}
|
|
80
|
+
</div>
|
|
81
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
82
|
+
{columns.map((column) => {
|
|
83
|
+
const checked = isChecked(row.key, column.key);
|
|
84
|
+
const pending = isPending(row.key, column.key);
|
|
85
|
+
const inputId = `check-matrix-${encodeURIComponent(row.key)}-${encodeURIComponent(column.key)}`;
|
|
86
|
+
return (
|
|
87
|
+
<label
|
|
88
|
+
key={column.key}
|
|
89
|
+
htmlFor={inputId}
|
|
90
|
+
className={`flex min-h-11 cursor-pointer items-center gap-3 rounded-md border px-3 py-2 text-sm focus-within:ring-2 focus-within:ring-indigo-500 focus-within:ring-offset-1 ${
|
|
91
|
+
checked
|
|
92
|
+
? "border-indigo-200 bg-indigo-50 text-indigo-950"
|
|
93
|
+
: "border-gray-200 bg-gray-50 text-gray-800"
|
|
94
|
+
} ${pending || disabled ? "cursor-not-allowed opacity-70" : "hover:border-indigo-300"}`}
|
|
95
|
+
>
|
|
96
|
+
<input
|
|
97
|
+
id={inputId}
|
|
98
|
+
type="checkbox"
|
|
99
|
+
checked={checked}
|
|
100
|
+
disabled={disabled || pending}
|
|
101
|
+
onChange={(event) =>
|
|
102
|
+
onToggle(row.key, column.key, event.target.checked)
|
|
103
|
+
}
|
|
104
|
+
className="h-5 w-5 shrink-0 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
|
105
|
+
/>
|
|
106
|
+
<span className="min-w-0 flex-1 break-words font-medium">
|
|
107
|
+
{column.label}
|
|
108
|
+
</span>
|
|
109
|
+
<span
|
|
110
|
+
className={`shrink-0 text-xs font-medium ${
|
|
111
|
+
checked ? "text-indigo-700" : "text-gray-600"
|
|
112
|
+
}`}
|
|
113
|
+
aria-live={pending ? "polite" : undefined}
|
|
114
|
+
>
|
|
115
|
+
{pending
|
|
116
|
+
? pendingLabel
|
|
117
|
+
: checked
|
|
118
|
+
? checkedLabel
|
|
119
|
+
: uncheckedLabel}
|
|
120
|
+
</span>
|
|
121
|
+
</label>
|
|
122
|
+
);
|
|
123
|
+
})}
|
|
124
|
+
</div>
|
|
125
|
+
</fieldset>
|
|
126
|
+
))}
|
|
127
|
+
{visibleCount < rows.length ? (
|
|
128
|
+
<button
|
|
129
|
+
type="button"
|
|
130
|
+
onClick={() => setVisibleCount((count) => count + safePageSize)}
|
|
131
|
+
className="min-h-11 w-full cursor-pointer rounded-md border border-indigo-300 bg-white px-4 py-2 text-sm font-semibold text-indigo-700 hover:bg-indigo-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2"
|
|
132
|
+
>
|
|
133
|
+
{showMoreLabel} ({rows.length - visibleCount})
|
|
134
|
+
</button>
|
|
135
|
+
) : null}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React, { ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
CheckMatrixCards,
|
|
4
|
+
CheckMatrixCardsProps,
|
|
5
|
+
} from "./CheckMatrixCards.js";
|
|
6
|
+
|
|
7
|
+
export interface ResponsiveCheckMatrixProps extends CheckMatrixCardsProps {
|
|
8
|
+
desktop: ReactNode;
|
|
9
|
+
desktopBreakpoint?: "sm" | "md" | "lg";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const visibilityClasses = {
|
|
13
|
+
sm: { mobile: "sm:hidden", desktop: "hidden sm:block" },
|
|
14
|
+
md: { mobile: "md:hidden", desktop: "hidden md:block" },
|
|
15
|
+
lg: { mobile: "lg:hidden", desktop: "hidden lg:block" },
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
export const ResponsiveCheckMatrix = ({
|
|
19
|
+
desktop,
|
|
20
|
+
desktopBreakpoint = "md",
|
|
21
|
+
...cardProps
|
|
22
|
+
}: ResponsiveCheckMatrixProps) => {
|
|
23
|
+
const visibility = visibilityClasses[desktopBreakpoint];
|
|
24
|
+
return (
|
|
25
|
+
<>
|
|
26
|
+
<div className={visibility.mobile}>
|
|
27
|
+
<CheckMatrixCards {...cardProps} />
|
|
28
|
+
</div>
|
|
29
|
+
<div className={visibility.desktop}>{desktop}</div>
|
|
30
|
+
</>
|
|
31
|
+
);
|
|
32
|
+
};
|
package/src/SidePanel/index.tsx
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
|
|
1
|
+
import React, {
|
|
2
|
+
useState,
|
|
3
|
+
ReactNode,
|
|
4
|
+
useEffect,
|
|
5
|
+
useId,
|
|
6
|
+
useRef,
|
|
7
|
+
} from "react";
|
|
8
|
+
import { motion, AnimatePresence, useReducedMotion } from "framer-motion";
|
|
3
9
|
|
|
4
10
|
/**
|
|
5
11
|
* SidePanel Component
|
|
@@ -14,11 +20,14 @@ import { motion, AnimatePresence } from "framer-motion";
|
|
|
14
20
|
* @returns A toggleable side panel component.
|
|
15
21
|
*/
|
|
16
22
|
|
|
17
|
-
interface SidePanelProps {
|
|
23
|
+
export interface SidePanelProps {
|
|
18
24
|
children: ReactNode;
|
|
19
25
|
closeOnClickOutside?: boolean;
|
|
20
26
|
widthPercentage?: number;
|
|
21
27
|
buttonIcon?: "chevron" | "filter" | ReactNode;
|
|
28
|
+
ariaLabel?: string;
|
|
29
|
+
openButtonLabel?: string;
|
|
30
|
+
closeButtonLabel?: string;
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
// SVG icon for the left-pointing chevron
|
|
@@ -28,6 +37,8 @@ const ChevronLeftIcon = () => (
|
|
|
28
37
|
viewBox="0 0 24 24"
|
|
29
38
|
fill="currentColor"
|
|
30
39
|
className="h-6 w-6"
|
|
40
|
+
aria-hidden="true"
|
|
41
|
+
focusable="false"
|
|
31
42
|
>
|
|
32
43
|
<path
|
|
33
44
|
fillRule="evenodd"
|
|
@@ -44,6 +55,8 @@ const ChevronRightIcon = () => (
|
|
|
44
55
|
viewBox="0 0 24 24"
|
|
45
56
|
fill="currentColor"
|
|
46
57
|
className="h-6 w-6"
|
|
58
|
+
aria-hidden="true"
|
|
59
|
+
focusable="false"
|
|
47
60
|
>
|
|
48
61
|
<path
|
|
49
62
|
fillRule="evenodd"
|
|
@@ -60,6 +73,8 @@ const FilterIcon = () => (
|
|
|
60
73
|
viewBox="0 0 24 24"
|
|
61
74
|
fill="currentColor"
|
|
62
75
|
className="h-6 w-6"
|
|
76
|
+
aria-hidden="true"
|
|
77
|
+
focusable="false"
|
|
63
78
|
>
|
|
64
79
|
<path
|
|
65
80
|
fillRule="evenodd"
|
|
@@ -74,17 +89,26 @@ export const SidePanel: React.FC<SidePanelProps> = ({
|
|
|
74
89
|
closeOnClickOutside = true,
|
|
75
90
|
widthPercentage = 25,
|
|
76
91
|
buttonIcon = "chevron",
|
|
92
|
+
ariaLabel = "Panel lateral",
|
|
93
|
+
openButtonLabel = "Abrir panel",
|
|
94
|
+
closeButtonLabel = "Cerrar panel",
|
|
77
95
|
}) => {
|
|
78
96
|
const [isOpen, setIsOpen] = useState(false);
|
|
79
97
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
98
|
+
const closeButtonRef = useRef<HTMLButtonElement>(null);
|
|
99
|
+
const triggerButtonRef = useRef<HTMLButtonElement>(null);
|
|
100
|
+
const wasOpenRef = useRef(false);
|
|
101
|
+
const panelId = useId();
|
|
102
|
+
const shouldReduceMotion = useReducedMotion();
|
|
80
103
|
|
|
81
104
|
const togglePanel = () => {
|
|
82
|
-
setIsOpen(!
|
|
105
|
+
setIsOpen((open) => !open);
|
|
83
106
|
};
|
|
84
107
|
|
|
85
108
|
useEffect(() => {
|
|
86
109
|
const handleClickOutside = (event: MouseEvent) => {
|
|
87
110
|
if (
|
|
111
|
+
isOpen &&
|
|
88
112
|
closeOnClickOutside &&
|
|
89
113
|
panelRef.current &&
|
|
90
114
|
!panelRef.current.contains(event.target as Node)
|
|
@@ -97,7 +121,39 @@ export const SidePanel: React.FC<SidePanelProps> = ({
|
|
|
97
121
|
return () => {
|
|
98
122
|
document.removeEventListener("mousedown", handleClickOutside);
|
|
99
123
|
};
|
|
100
|
-
}, [closeOnClickOutside]);
|
|
124
|
+
}, [closeOnClickOutside, isOpen]);
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (!isOpen) return;
|
|
128
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
129
|
+
if (event.key === "Escape") setIsOpen(false);
|
|
130
|
+
};
|
|
131
|
+
document.addEventListener("keydown", handleEscape);
|
|
132
|
+
return () => document.removeEventListener("keydown", handleEscape);
|
|
133
|
+
}, [isOpen]);
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (isOpen) panelRef.current?.removeAttribute("inert");
|
|
137
|
+
else panelRef.current?.setAttribute("inert", "");
|
|
138
|
+
}, [isOpen]);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (isOpen) {
|
|
142
|
+
wasOpenRef.current = true;
|
|
143
|
+
closeButtonRef.current?.focus();
|
|
144
|
+
const previousOverflow = document.body.style.overflow;
|
|
145
|
+
if (window.matchMedia("(max-width: 767px)").matches) {
|
|
146
|
+
document.body.style.overflow = "hidden";
|
|
147
|
+
}
|
|
148
|
+
return () => {
|
|
149
|
+
document.body.style.overflow = previousOverflow;
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (wasOpenRef.current) {
|
|
153
|
+
wasOpenRef.current = false;
|
|
154
|
+
triggerButtonRef.current?.focus();
|
|
155
|
+
}
|
|
156
|
+
}, [isOpen]);
|
|
101
157
|
|
|
102
158
|
const renderIcon = () => {
|
|
103
159
|
if (typeof buttonIcon === "string") {
|
|
@@ -117,30 +173,63 @@ export const SidePanel: React.FC<SidePanelProps> = ({
|
|
|
117
173
|
return (
|
|
118
174
|
<>
|
|
119
175
|
<motion.div
|
|
176
|
+
id={panelId}
|
|
120
177
|
ref={panelRef}
|
|
178
|
+
role="dialog"
|
|
179
|
+
aria-modal={isOpen ? "true" : undefined}
|
|
180
|
+
aria-label={ariaLabel}
|
|
181
|
+
aria-hidden={!isOpen}
|
|
121
182
|
initial={{ x: "100%" }}
|
|
122
183
|
animate={{ x: isOpen ? 0 : "100%" }}
|
|
123
|
-
transition={
|
|
124
|
-
|
|
125
|
-
|
|
184
|
+
transition={
|
|
185
|
+
shouldReduceMotion
|
|
186
|
+
? { duration: 0 }
|
|
187
|
+
: { type: "spring", stiffness: 300, damping: 30 }
|
|
188
|
+
}
|
|
189
|
+
className="fixed right-0 top-0 z-[9999] h-full w-[calc(100%-1rem)] overflow-y-auto bg-white shadow-lg sm:w-[min(28rem,calc(100%-2rem))] md:w-[var(--side-panel-width)]"
|
|
190
|
+
style={
|
|
191
|
+
{
|
|
192
|
+
"--side-panel-width": `${widthPercentage}%`,
|
|
193
|
+
} as React.CSSProperties
|
|
194
|
+
}
|
|
126
195
|
>
|
|
127
196
|
<button
|
|
197
|
+
ref={closeButtonRef}
|
|
198
|
+
type="button"
|
|
128
199
|
onClick={togglePanel}
|
|
129
|
-
|
|
200
|
+
aria-label={closeButtonLabel}
|
|
201
|
+
className="absolute left-4 top-4 min-h-11 min-w-11 cursor-pointer rounded-full bg-gray-200 p-2 transition-colors hover:bg-gray-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 md:min-h-0 md:min-w-0"
|
|
130
202
|
>
|
|
131
203
|
<ChevronRightIcon />
|
|
132
204
|
</button>
|
|
133
|
-
<div className="p-
|
|
205
|
+
<div className="mt-16 p-4 sm:p-6">{children}</div>
|
|
134
206
|
</motion.div>
|
|
135
207
|
<AnimatePresence>
|
|
208
|
+
{isOpen && closeOnClickOutside ? (
|
|
209
|
+
<motion.button
|
|
210
|
+
type="button"
|
|
211
|
+
aria-label={closeButtonLabel}
|
|
212
|
+
initial={{ opacity: 0 }}
|
|
213
|
+
animate={{ opacity: 1 }}
|
|
214
|
+
exit={{ opacity: 0 }}
|
|
215
|
+
transition={{ duration: shouldReduceMotion ? 0 : 0.15 }}
|
|
216
|
+
onClick={() => setIsOpen(false)}
|
|
217
|
+
className="fixed inset-0 z-[9998] cursor-default bg-black/30 md:hidden"
|
|
218
|
+
/>
|
|
219
|
+
) : null}
|
|
136
220
|
{!isOpen && (
|
|
137
221
|
<motion.button
|
|
222
|
+
ref={triggerButtonRef}
|
|
223
|
+
type="button"
|
|
224
|
+
aria-label={openButtonLabel}
|
|
225
|
+
aria-expanded={isOpen}
|
|
226
|
+
aria-controls={panelId}
|
|
138
227
|
initial={{ opacity: 0, x: 20 }}
|
|
139
228
|
animate={{ opacity: 1, x: 0 }}
|
|
140
229
|
exit={{ opacity: 0, x: 20 }}
|
|
141
|
-
transition={{ duration: 0.2 }}
|
|
230
|
+
transition={{ duration: shouldReduceMotion ? 0 : 0.2 }}
|
|
142
231
|
onClick={togglePanel}
|
|
143
|
-
className=
|
|
232
|
+
className="fixed right-0 top-1/2 z-[10000] min-h-11 min-w-11 -translate-y-1/2 cursor-pointer rounded-l-lg bg-indigo-600 p-3 text-white shadow-md transition-colors hover:bg-indigo-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2"
|
|
144
233
|
>
|
|
145
234
|
{renderIcon()}
|
|
146
235
|
</motion.button>
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from "./AccessControl/index.js";
|
|
|
3
3
|
export * from "./BlockUI/index.js";
|
|
4
4
|
export * from "./Buttons/index.js";
|
|
5
5
|
export * from "./Card/index.js";
|
|
6
|
+
export * from "./Matrix/index.js";
|
|
6
7
|
export * from "./ConnectionState/index.js";
|
|
7
8
|
export * from "./Chat/index.js";
|
|
8
9
|
export * from "./Containers/index.js";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { ResponsiveCheckMatrix } from "../src/Matrix/ResponsiveCheckMatrix.js";
|
|
3
|
+
|
|
4
|
+
const rows = [
|
|
5
|
+
{ key: "129", label: "129 · Cooperativa El Progreso", description: "San Juan" },
|
|
6
|
+
{ key: "302", label: "302 · Productor con un nombre largo para validar el ajuste de texto" },
|
|
7
|
+
];
|
|
8
|
+
const columns = [
|
|
9
|
+
{ key: "brocoli", label: "Brócoli certificado" },
|
|
10
|
+
{ key: "radicchio", label: "Radicchio" },
|
|
11
|
+
{ key: "elotin", label: "Elotín" },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export const Responsive = () => {
|
|
15
|
+
const [blocked, setBlocked] = useState(new Set(["129:radicchio"]));
|
|
16
|
+
const isChecked = (rowKey: string, columnKey: string) =>
|
|
17
|
+
!blocked.has(`${rowKey}:${columnKey}`);
|
|
18
|
+
const onToggle = (rowKey: string, columnKey: string, checked: boolean) => {
|
|
19
|
+
setBlocked((current) => {
|
|
20
|
+
const next = new Set(current);
|
|
21
|
+
const key = `${rowKey}:${columnKey}`;
|
|
22
|
+
if (checked) next.delete(key);
|
|
23
|
+
else next.add(key);
|
|
24
|
+
return next;
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const desktop = (
|
|
29
|
+
<div className="overflow-x-auto rounded-md border border-gray-200">
|
|
30
|
+
<table className="min-w-full text-sm">
|
|
31
|
+
<thead className="bg-gray-50">
|
|
32
|
+
<tr>
|
|
33
|
+
<th className="px-3 py-2 text-left">Proveedor</th>
|
|
34
|
+
{columns.map((column) => (
|
|
35
|
+
<th key={column.key} className="px-3 py-2 text-center">
|
|
36
|
+
{column.label}
|
|
37
|
+
</th>
|
|
38
|
+
))}
|
|
39
|
+
</tr>
|
|
40
|
+
</thead>
|
|
41
|
+
<tbody>
|
|
42
|
+
{rows.map((row) => (
|
|
43
|
+
<tr key={row.key} className="border-t border-gray-200">
|
|
44
|
+
<th className="px-3 py-2 text-left">{row.label}</th>
|
|
45
|
+
{columns.map((column) => (
|
|
46
|
+
<td key={column.key} className="px-3 py-2 text-center">
|
|
47
|
+
<input
|
|
48
|
+
type="checkbox"
|
|
49
|
+
aria-label={`${String(row.label)}, ${String(column.label)}`}
|
|
50
|
+
checked={isChecked(row.key, column.key)}
|
|
51
|
+
onChange={(event) =>
|
|
52
|
+
onToggle(row.key, column.key, event.target.checked)
|
|
53
|
+
}
|
|
54
|
+
/>
|
|
55
|
+
</td>
|
|
56
|
+
))}
|
|
57
|
+
</tr>
|
|
58
|
+
))}
|
|
59
|
+
</tbody>
|
|
60
|
+
</table>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<ResponsiveCheckMatrix
|
|
66
|
+
rows={rows}
|
|
67
|
+
columns={columns}
|
|
68
|
+
desktop={desktop}
|
|
69
|
+
isChecked={isChecked}
|
|
70
|
+
onToggle={onToggle}
|
|
71
|
+
checkedLabel="Permitida"
|
|
72
|
+
uncheckedLabel="Bloqueada"
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { SidePanel } from "../src/SidePanel/index.js";
|
|
2
|
+
|
|
3
|
+
export const Filters = () => (
|
|
4
|
+
<div className="min-h-[36rem] bg-gray-50 p-6">
|
|
5
|
+
<p className="text-sm text-gray-700">
|
|
6
|
+
Use el botón fijo para abrir los filtros.
|
|
7
|
+
</p>
|
|
8
|
+
<SidePanel buttonIcon="filter" ariaLabel="Filtros de búsqueda">
|
|
9
|
+
<h2 className="text-lg font-semibold text-gray-900">
|
|
10
|
+
Filtros de búsqueda
|
|
11
|
+
</h2>
|
|
12
|
+
<label className="mt-4 block text-sm font-medium text-gray-700">
|
|
13
|
+
Proveedor
|
|
14
|
+
<input
|
|
15
|
+
className="mt-1 block w-full rounded-md border-gray-300"
|
|
16
|
+
placeholder="Nombre o código"
|
|
17
|
+
/>
|
|
18
|
+
</label>
|
|
19
|
+
</SidePanel>
|
|
20
|
+
</div>
|
|
21
|
+
);
|