next-recomponents 2.0.40 → 2.0.42

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.
@@ -0,0 +1,335 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { createPortal } from "react-dom";
3
+ import DropIcon, {
4
+ ArrowDownIcon,
5
+ ArrowUPIcon,
6
+ FilterIcon,
7
+ OrderAscIcon,
8
+ OrderDesIcon,
9
+ } from "./icons";
10
+ import { MenuItem } from "./menu.item";
11
+ import Form from "../form";
12
+ import { ContextType } from "./context";
13
+ import { TableProps } from "./types";
14
+
15
+ export function Header({
16
+ header,
17
+ context,
18
+ drag,
19
+ padding,
20
+ }: {
21
+ header: string;
22
+ context: ContextType & Omit<TableProps, "data">;
23
+ drag: {
24
+ widths: Record<string, number>;
25
+ gridTemplateColumns: string;
26
+ startDrag: (e: React.MouseEvent, headerKey: string) => void;
27
+ };
28
+ padding: string;
29
+ }) {
30
+ const [visible, setVisible] = useState(false);
31
+
32
+ const sortIndex = context.sort.findIndex((b) => b.field == header);
33
+ const [text, setText] = useState("");
34
+ const cellRef = useRef<HTMLDivElement>(null);
35
+ const [coords, setCoords] = useState<{ top: number; left: number } | null>(
36
+ null,
37
+ );
38
+
39
+ const filteredValues = [
40
+ ...new Set(context.data.map((d) => d[header])),
41
+ ].filter((d) => {
42
+ if (text == "") return true;
43
+ return `${d}`.toLowerCase().includes(text.toLowerCase());
44
+ });
45
+
46
+ const [selected, setSelected] = useState([...filteredValues]);
47
+
48
+ const hasFilter = context.filters.find((b) => b.field == header);
49
+
50
+ useEffect(() => {
51
+ if (visible) {
52
+ context.setCurrentHeader(header);
53
+ }
54
+ setText("");
55
+ }, [visible]);
56
+
57
+ // Calcula la posición del popup relativa al viewport cada vez que se abre,
58
+ // y la vuelve a calcular en scroll/resize mientras está abierto.
59
+ useEffect(() => {
60
+ if (!visible || !cellRef.current) {
61
+ setCoords(null);
62
+ return;
63
+ }
64
+
65
+ const updateCoords = () => {
66
+ if (!cellRef.current) return;
67
+ const rect = cellRef.current.getBoundingClientRect();
68
+ const POPUP_WIDTH = 300;
69
+ setCoords({
70
+ top: rect.bottom,
71
+ left: Math.max(8, rect.right - POPUP_WIDTH),
72
+ });
73
+ };
74
+
75
+ updateCoords();
76
+ window.addEventListener("scroll", updateCoords, true);
77
+ window.addEventListener("resize", updateCoords);
78
+ return () => {
79
+ window.removeEventListener("scroll", updateCoords, true);
80
+ window.removeEventListener("resize", updateCoords);
81
+ };
82
+ }, [visible]);
83
+
84
+ if (header == "__modal__") {
85
+ return (
86
+ <div className="">
87
+ <div
88
+ className={"break-words border text-center relative "}
89
+ style={{ zIndex: 10, height: `${padding}px`, maxHeight: "100px" }}
90
+ ></div>
91
+ </div>
92
+ );
93
+ } else if (header == "__select__") {
94
+ return (
95
+ <div className="">
96
+ <div
97
+ className={
98
+ "border flex items-center justify-center text-center relative "
99
+ }
100
+ style={{ zIndex: 10, height: `${padding}px`, maxHeight: "100px" }}
101
+ >
102
+ <input
103
+ type="checkbox"
104
+ className="w-5 h-5 accent-blue-600 transition-all duration-300 checked:scale-110"
105
+ checked={Boolean(
106
+ context.selected.length == context.filteredData.length,
107
+ )}
108
+ onChange={(e) => {
109
+ if (context.selected.length < context.filteredData.length) {
110
+ context.setSelected(context.filteredData.map((d) => d.id));
111
+ } else {
112
+ context.setSelected([]);
113
+ }
114
+ }}
115
+ />
116
+ </div>
117
+ </div>
118
+ );
119
+ } else
120
+ return (
121
+ <div className="">
122
+ <div
123
+ ref={cellRef}
124
+ className={"break-words border text-center relative "}
125
+ style={{ zIndex: 10, height: `${padding}px`, maxHeight: "100px" }}
126
+ >
127
+ <div className="flex gap-5 items-center justify-between h-full cursor-pointer">
128
+ <div className="flex gap-2">
129
+ {hasFilter ? (
130
+ <div
131
+ className="cursor-pointer"
132
+ onClick={(e) => {
133
+ context.setFilters(null);
134
+ setSelected(filteredValues);
135
+ }}
136
+ >
137
+ <FilterIcon />
138
+ </div>
139
+ ) : (
140
+ <div></div>
141
+ )}
142
+ <div
143
+ onClick={(e) => {
144
+ context.setSort({
145
+ [header]: "TOGGLE",
146
+ });
147
+ }}
148
+ >
149
+ {sortIndex >= 0 ? (
150
+ context.sort[sortIndex].type === "ASC" ? (
151
+ <ArrowUPIcon />
152
+ ) : (
153
+ <ArrowDownIcon />
154
+ )
155
+ ) : (
156
+ <div></div>
157
+ )}
158
+ </div>
159
+ </div>
160
+ <div
161
+ className="w-full"
162
+ onClick={(e) => {
163
+ context.setSort({
164
+ [header]: "TOGGLE",
165
+ });
166
+ }}
167
+ >
168
+ {header}
169
+ </div>
170
+ <div
171
+ style={{ zIndex: 10 }}
172
+ className="icon font-bold p-1 cursor-pointer pr-3 "
173
+ onClick={(e) => setVisible(true)}
174
+ >
175
+ <DropIcon />
176
+ </div>
177
+ </div>
178
+
179
+ {context.currentHeader != header && (
180
+ <div
181
+ onMouseDown={(e) => drag.startDrag(e, header)}
182
+ className="absolute top-0 h-full cursor-col-resize hover:bg-blue-300 p-2"
183
+ style={{ right: -2, width: 6, zIndex: 20 }}
184
+ />
185
+ )}
186
+
187
+ {visible &&
188
+ context.currentHeader == header &&
189
+ createPortal(
190
+ <>
191
+ {/* Overlay a pantalla completa para cerrar al hacer click fuera */}
192
+ <div
193
+ className="w-screen h-screen fixed top-0 left-0"
194
+ onClick={(e) => {
195
+ setVisible(false);
196
+ }}
197
+ style={{ zIndex: 9998 }}
198
+ />
199
+
200
+ {/* Popup posicionado con fixed + coordenadas calculadas del header real */}
201
+ {coords && (
202
+ <div
203
+ className="fixed"
204
+ style={{ top: coords.top, left: coords.left, zIndex: 9999 }}
205
+ >
206
+ <div className="bg-gray-100 w-[300px] flex flex-col border shadow rounded ">
207
+ {hasFilter && (
208
+ <MenuItem
209
+ label="Quitar filtro"
210
+ icon={<FilterIcon />}
211
+ onClick={(e) => {
212
+ setVisible(false);
213
+ context.setFilters(null);
214
+ setSelected(filteredValues);
215
+ }}
216
+ />
217
+ )}
218
+ <MenuItem
219
+ label="Orenar de A a Z"
220
+ icon={<OrderAscIcon />}
221
+ onClick={(e) => {
222
+ setVisible(false);
223
+ context.setSort({ [header]: "ASC" });
224
+ context.setCurrentHeader(null);
225
+ }}
226
+ />
227
+ <MenuItem
228
+ label="Orenar de Z a A"
229
+ icon={<OrderDesIcon />}
230
+ onClick={(e) => {
231
+ setVisible(false);
232
+ context.setSort({ [header]: "DESC" });
233
+ context.setCurrentHeader(null);
234
+ }}
235
+ />
236
+ <Form
237
+ onSubmit={(e) => {
238
+ if (text != "") {
239
+ setSelected(filteredValues);
240
+ }
241
+ context.setFilters({
242
+ field: header,
243
+ values: text != "" ? filteredValues : selected,
244
+ });
245
+ setVisible(false);
246
+ context.setCurrentHeader(null);
247
+ }}
248
+ >
249
+ <div className="m-2 bg-white gap-2 flex flex-col">
250
+ <input
251
+ placeholder="Buscar..."
252
+ className="border shadow rounded p-2 w-full"
253
+ value={text}
254
+ onChange={(e) => setText(e.target.value)}
255
+ />
256
+ <div
257
+ className="flex items-start flex-col p-2 border shadow rounded max-w-[600px] h-[300px] bg-white "
258
+ style={{ overflow: "auto" }}
259
+ >
260
+ <div
261
+ className={
262
+ "flex gap-2 " +
263
+ (text != "" ? "text-gray-400" : "")
264
+ }
265
+ >
266
+ <input
267
+ type="checkbox"
268
+ checked={
269
+ text != "" ||
270
+ selected.length == filteredValues.length
271
+ }
272
+ onClick={(e) => {
273
+ if (
274
+ selected.length == filteredValues.length
275
+ ) {
276
+ setSelected([]);
277
+ } else {
278
+ setSelected(filteredValues);
279
+ }
280
+ }}
281
+ disabled={text != ""}
282
+ />
283
+ <div className="truncate">Seleccionar todo</div>
284
+ </div>
285
+
286
+ {filteredValues.map((d) => {
287
+ return (
288
+ <div key={d} className="flex gap-2 ">
289
+ <input
290
+ disabled={text != ""}
291
+ type="checkbox"
292
+ onChange={(e) => {
293
+ const ns = [...selected];
294
+ const index = ns.findIndex((n) => n == d);
295
+ if (index >= 0) {
296
+ ns.splice(index, 1);
297
+ } else {
298
+ ns.push(d);
299
+ }
300
+ setSelected(ns);
301
+ }}
302
+ checked={text != "" || selected.includes(d)}
303
+ />
304
+ <div className="truncate">{d}</div>
305
+ </div>
306
+ );
307
+ })}
308
+ </div>
309
+ </div>
310
+ <div className="flex justify-between p-2">
311
+ <button
312
+ onClick={(e) => {
313
+ setVisible(false);
314
+ context.setCurrentHeader(null);
315
+ }}
316
+ className="p-2 border shadow rounded bg-gray-300 text-black"
317
+ type="button"
318
+ >
319
+ Cancelar
320
+ </button>
321
+ <button className="p-2 border shadow rounded bg-blue-500 text-white">
322
+ Aceptar
323
+ </button>
324
+ </div>
325
+ </Form>
326
+ </div>
327
+ </div>
328
+ )}
329
+ </>,
330
+ document.body,
331
+ )}
332
+ </div>
333
+ </div>
334
+ );
335
+ }
@@ -0,0 +1,167 @@
1
+ export default function DropIcon() {
2
+ return (
3
+ <svg
4
+ stroke="currentColor"
5
+ fill="currentColor"
6
+ strokeWidth="0"
7
+ viewBox="0 0 512 512"
8
+ height="1em"
9
+ className="text-blue-500"
10
+ width="1em"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ >
13
+ <circle cx="256" cy="256" r="48"></circle>
14
+ <circle cx="256" cy="416" r="48"></circle>
15
+ <circle cx="256" cy="96" r="48"></circle>
16
+ </svg>
17
+ );
18
+ }
19
+ export function EditIcon() {
20
+ return (
21
+ <svg
22
+ className="text-white"
23
+ stroke="currentColor"
24
+ fill="currentColor"
25
+ strokeWidth="0"
26
+ viewBox="0 0 24 24"
27
+ height="1em"
28
+ width="1em"
29
+ xmlns="http://www.w3.org/2000/svg"
30
+ >
31
+ <path d="M16.7574 2.99678L14.7574 4.99678H5V18.9968H19V9.23943L21 7.23943V19.9968C21 20.5491 20.5523 20.9968 20 20.9968H4C3.44772 20.9968 3 20.5491 3 19.9968V3.99678C3 3.4445 3.44772 2.99678 4 2.99678H16.7574ZM20.4853 2.09729L21.8995 3.5115L12.7071 12.7039L11.2954 12.7064L11.2929 11.2897L20.4853 2.09729Z"></path>
32
+ </svg>
33
+ );
34
+ }
35
+ export function ExcelIcon() {
36
+ return (
37
+ <svg
38
+ stroke="currentColor"
39
+ fill="currentColor"
40
+ strokeWidth="0"
41
+ viewBox="0 0 24 24"
42
+ height="1em"
43
+ width="1em"
44
+ xmlns="http://www.w3.org/2000/svg"
45
+ >
46
+ <path d="M2.85858 2.87732L15.4293 1.0815C15.7027 1.04245 15.9559 1.2324 15.995 1.50577C15.9983 1.52919 16 1.55282 16 1.57648V22.4235C16 22.6996 15.7761 22.9235 15.5 22.9235C15.4763 22.9235 15.4527 22.9218 15.4293 22.9184L2.85858 21.1226C2.36593 21.0522 2 20.6303 2 20.1327V3.86727C2 3.36962 2.36593 2.9477 2.85858 2.87732ZM4 4.73457V19.2654L14 20.694V3.30599L4 4.73457ZM17 19H20V4.99997H17V2.99997H21C21.5523 2.99997 22 3.44769 22 3.99997V20C22 20.5523 21.5523 21 21 21H17V19ZM10.2 12L13 16H10.6L9 13.7143L7.39999 16H5L7.8 12L5 7.99997H7.39999L9 10.2857L10.6 7.99997H13L10.2 12Z"></path>
47
+ </svg>
48
+ );
49
+ }
50
+ export function OrderAscIcon() {
51
+ return (
52
+ <svg
53
+ stroke="currentColor"
54
+ fill="currentColor"
55
+ strokeWidth="0"
56
+ version="1"
57
+ viewBox="0 0 48 48"
58
+ enableBackground="new 0 0 48 48"
59
+ height="1em"
60
+ width="1em"
61
+ xmlns="http://www.w3.org/2000/svg"
62
+ >
63
+ <polygon
64
+ fill="#546E7A"
65
+ points="38,33 38,5 34,5 34,33 28,33 36,43 44,33"
66
+ ></polygon>
67
+ <g fill="#2196F3">
68
+ <path d="M16.8,17.2h-5.3l-1.1,3H6.9L12.6,5h2.9l5.7,15.2h-3.2L16.8,17.2z M12.2,14.5H16l-1.9-5.7L12.2,14.5z"></path>
69
+ <path d="M12.4,40.5H20V43H8.4v-1.9L16,30.3H8.4v-2.5h11.4v1.7L12.4,40.5z"></path>
70
+ </g>
71
+ </svg>
72
+ );
73
+ }
74
+
75
+ export function OrderDesIcon() {
76
+ return (
77
+ <svg
78
+ stroke="currentColor"
79
+ fill="currentColor"
80
+ strokeWidth="0"
81
+ version="1"
82
+ viewBox="0 0 48 48"
83
+ enableBackground="new 0 0 48 48"
84
+ height="1em"
85
+ width="1em"
86
+ className="text-blue-500"
87
+ xmlns="http://www.w3.org/2000/svg"
88
+ >
89
+ <g fill="#2196F3">
90
+ <path d="M16.8,40h-5.3l-1.1,3H6.9l5.7-15.2h2.9L21.1,43h-3.2L16.8,40z M12.2,37.3H16l-1.9-5.7L12.2,37.3z"></path>
91
+ <path d="M12.4,17.7H20v2.5H8.4v-1.9L16,7.5H8.4V5h11.4v1.7L12.4,17.7z"></path>
92
+ </g>
93
+ <polygon
94
+ fill="#546E7A"
95
+ points="38,33 38,5 34,5 34,33 28,33 36,43 44,33"
96
+ ></polygon>
97
+ </svg>
98
+ );
99
+ }
100
+
101
+ export function ArrowDownIcon() {
102
+ return (
103
+ <svg
104
+ stroke="currentColor"
105
+ fill="currentColor"
106
+ strokeWidth="0"
107
+ className="text-blue-500"
108
+ viewBox="0 0 512 512"
109
+ height="1em"
110
+ width="1em"
111
+ xmlns="http://www.w3.org/2000/svg"
112
+ >
113
+ <path d="M348.3 295.6c-5-5.1-13.3-5.1-18.4-.1L269 356.2V124.9c0-7.1-5.8-12.9-13-12.9s-13 5.8-13 12.9v231.3l-60.9-60.8c-5.1-5-13.3-4.9-18.4.1-5 5.1-5 13.2.1 18.3l83 82.4c1.2 1.1 2.5 2 4.1 2.7 1.6.7 3.3 1 5 1 3.4 0 6.6-1.3 9.1-3.7l83-82.4c5.2-4.9 5.3-13.1.3-18.2z"></path>
114
+ </svg>
115
+ );
116
+ }
117
+
118
+ export function ArrowUPIcon() {
119
+ return (
120
+ <svg
121
+ stroke="currentColor"
122
+ fill="currentColor"
123
+ strokeWidth="0"
124
+ className="text-blue-500"
125
+ viewBox="0 0 512 512"
126
+ height="1em"
127
+ width="1em"
128
+ xmlns="http://www.w3.org/2000/svg"
129
+ >
130
+ <path d="M348.3 216.4c-5 5.1-13.3 5.1-18.4.1L269 155.8v231.3c0 7.1-5.8 12.9-13 12.9s-13-5.8-13-12.9V155.8l-60.9 60.8c-5.1 5-13.3 4.9-18.4-.1-5-5.1-5-13.2.1-18.3l83-82.4c1.2-1.1 2.5-2 4.1-2.7 1.6-.7 3.3-1 5-1 3.4 0 6.6 1.3 9.1 3.7l83 82.4c5.2 4.9 5.3 13.1.3 18.2z"></path>
131
+ </svg>
132
+ );
133
+ }
134
+ export function SearchIcon() {
135
+ return (
136
+ <svg
137
+ stroke="currentColor"
138
+ fill="currentColor"
139
+ strokeWidth="0"
140
+ viewBox="0 0 24 24"
141
+ className="text-blue-500"
142
+ height="1em"
143
+ width="1em"
144
+ xmlns="http://www.w3.org/2000/svg"
145
+ >
146
+ <path fill="none" d="M0 0h24v24H0z"></path>
147
+ <path d="M7 9H2V7h5zm0 3H2v2h5zm13.59 7-3.83-3.83c-.8.52-1.74.83-2.76.83-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5c0 1.02-.31 1.96-.83 2.75L22 17.59zM17 11c0-1.65-1.35-3-3-3s-3 1.35-3 3 1.35 3 3 3 3-1.35 3-3M2 19h10v-2H2z"></path>
148
+ </svg>
149
+ );
150
+ }
151
+ export function FilterIcon() {
152
+ return (
153
+ <svg
154
+ className="text-red-300"
155
+ stroke="currentColor"
156
+ fill="currentColor"
157
+ strokeWidth="0"
158
+ viewBox="0 0 24 24"
159
+ height="1em"
160
+ width="1em"
161
+ xmlns="http://www.w3.org/2000/svg"
162
+ >
163
+ <path fill="none" d="M0 0h24v24H0zM0 0h24m0 24H0"></path>
164
+ <path d="M4.25 5.66c.1.13 5.74 7.33 5.74 7.33V19c0 .55.45 1 1.01 1h2.01c.55 0 1.01-.45 1.01-1v-6.02s5.49-7.02 5.75-7.34S20 5 20 5c0-.55-.45-1-1.01-1H5.01C4.4 4 4 4.48 4 5c0 .2.06.44.25.66"></path>
165
+ </svg>
166
+ );
167
+ }
@@ -0,0 +1,43 @@
1
+ import { ReactNode, useEffect, useMemo, useReducer, useState } from "react";
2
+ import regularExpresions, { FiltersType, SortType, TableProps } from "./types";
3
+ import HTable from "./h.table";
4
+ import useContext from "./context";
5
+
6
+ export default function TableAdvanced(tableProps: TableProps) {
7
+ const { data, ...props } = tableProps;
8
+
9
+ const context = useContext({ onSelect: Boolean(tableProps.onSelect) });
10
+
11
+ useEffect(() => {
12
+ context.setData(
13
+ Array.isArray(data)
14
+ ? data.map((d) => {
15
+ return {
16
+ ...(tableProps?.modal ? { __modal__: "" } : {}),
17
+ ...(tableProps?.onSelect ? { __select__: "" } : {}),
18
+ ...d,
19
+ };
20
+ })
21
+ : data,
22
+ );
23
+ }, [data]);
24
+
25
+ useEffect(() => {
26
+ if (tableProps?.sortBy) {
27
+ const [k, v] = Object.entries(tableProps.sortBy)[0];
28
+
29
+ context.setSort({ [k]: v });
30
+ }
31
+ }, [tableProps?.sortBy]);
32
+
33
+ if (Array.isArray(data) && data.length > 0) {
34
+ if (data.every((d) => !d?.id)) {
35
+ throw new Error("All data rows must have an 'id' property.");
36
+ }
37
+ if (props?.onSave && props.onSelect) {
38
+ throw new Error("Must have only one of onSave or onSelect");
39
+ }
40
+ return <HTable context={{ ...context, ...props }} />;
41
+ }
42
+ return null;
43
+ }
@@ -0,0 +1,21 @@
1
+ import { ReactNode } from "react";
2
+
3
+ export function MenuItem({
4
+ label,
5
+ icon,
6
+ onClick,
7
+ }: {
8
+ label: string;
9
+ icon: ReactNode;
10
+ onClick: React.MouseEventHandler<HTMLDivElement>;
11
+ }) {
12
+ return (
13
+ <div
14
+ className="cursor-pointer border p-2 hover:bg-gray-100 flex items-center bg-white gap-2"
15
+ onClick={onClick}
16
+ >
17
+ {icon}
18
+ {label}
19
+ </div>
20
+ );
21
+ }
@@ -0,0 +1,24 @@
1
+ import { TableProps } from "@mui/material";
2
+ import { ContextType } from "./context";
3
+ import { SearchIcon } from "./icons";
4
+
5
+ export default function Searchable({
6
+ context,
7
+ }: {
8
+ context: ContextType & Omit<TableProps, "data">;
9
+ }) {
10
+ return (
11
+ <div className="flex justify-start p-2 relative w-96">
12
+ <input
13
+ type="search"
14
+ placeholder="Buscar por ...."
15
+ className="border shadow rounded p-2 w-full pr-10"
16
+ value={context.searchBy}
17
+ onChange={(e) => context.setSearchBy(e.target.value)}
18
+ />
19
+ <div className="absolute top-0 right-0 p-5">
20
+ <SearchIcon />
21
+ </div>
22
+ </div>
23
+ );
24
+ }
@@ -0,0 +1,29 @@
1
+ import { SortType } from "./types";
2
+
3
+ export const sortReducer = (
4
+ acc: SortType[],
5
+ b: Record<string, "ASC" | "DESC" | "TOGGLE">,
6
+ ) => {
7
+ const oldAcc = [...acc];
8
+ const newAcc = [];
9
+ const keys = Object.keys(b);
10
+ const k = keys[0];
11
+
12
+ let newType = b[k];
13
+ if (b[k] == "TOGGLE") {
14
+ newType = oldAcc?.[0]?.type == "ASC" ? "DESC" : "ASC";
15
+ }
16
+ newAcc.push({ field: k, type: newType });
17
+
18
+ // const newAcc = [...acc];
19
+ // const keys = Object.keys(b);
20
+ // for (let k of keys) {
21
+ // const index = newAcc.findIndex((aa) => aa.field == k);
22
+ // if (index >= 0) {
23
+ // newAcc.splice(index, 1);
24
+ // }
25
+
26
+ // newAcc.push({ field: k, type: b[k] });
27
+ // }
28
+ return newAcc;
29
+ };