next-recomponents 1.0.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/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "next-recomponents",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "build": "tsup src/index.tsx --format cjs,esm --dts --clean",
8
+ "prepublishOnly": "npm run build",
9
+ "publish:public": "npm publish --access public"
10
+ },
11
+ "keywords": [],
12
+ "author": "",
13
+ "license": "ISC",
14
+ "devDependencies": {
15
+ "tsup": "^8.5.0",
16
+ "typescript": "^5.8.3"
17
+ }
18
+ }
@@ -0,0 +1,24 @@
1
+ import { catColor } from "../button/colors";
2
+
3
+ interface Props {
4
+ color?:
5
+ | "white"
6
+ | "primary"
7
+ | "secondary"
8
+ | "info"
9
+ | "danger"
10
+ | "warning"
11
+ | "success";
12
+ children: React.ReactNode;
13
+ }
14
+ export default function Alert({
15
+ color = "primary",
16
+ children,
17
+ ...props
18
+ }: Props) {
19
+ return (
20
+ <div className={[catColor[color], "p-2 rounded shadow border"].join(" ")}>
21
+ {children}
22
+ </div>
23
+ );
24
+ }
@@ -0,0 +1,9 @@
1
+ export const catColor = {
2
+ white: "bg-white text-black",
3
+ primary: "bg-blue-500 text-white",
4
+ secondary: "bg-blue-800 text-white",
5
+ info: "bg-blue-200 text-black",
6
+ danger: "bg-red-500 text-white",
7
+ warning: "bg-yellow-500 text-white",
8
+ success: "bg-green-500 text-white",
9
+ };
@@ -0,0 +1,42 @@
1
+ import { ButtonHTMLAttributes, DetailedHTMLProps } from "react";
2
+ import { catColor } from "./colors";
3
+
4
+ interface Props
5
+ extends DetailedHTMLProps<
6
+ ButtonHTMLAttributes<HTMLButtonElement>,
7
+ HTMLButtonElement
8
+ > {
9
+ icon?: React.ReactNode;
10
+ size?: "full" | "small";
11
+ color?:
12
+ | "white"
13
+ | "primary"
14
+ | "secondary"
15
+ | "info"
16
+ | "danger"
17
+ | "warning"
18
+ | "success";
19
+ }
20
+ export default function Button({
21
+ className,
22
+ size = "small",
23
+ color = "primary",
24
+ children,
25
+ icon,
26
+ ...props
27
+ }: Props) {
28
+ return (
29
+ <button
30
+ {...props}
31
+ className={[
32
+ className,
33
+ "p-2 border shadow rounded flex gap-1 justify-center items-center",
34
+ size == "full" ? "w-full" : "",
35
+ catColor[color],
36
+ ].join(" ")}
37
+ >
38
+ {icon}
39
+ {children}
40
+ </button>
41
+ );
42
+ }
@@ -0,0 +1,65 @@
1
+ export default function MenuIcon() {
2
+ return (
3
+ <svg
4
+ stroke="currentColor"
5
+ fill="currentColor"
6
+ strokeWidth="0"
7
+ viewBox="0 0 512 512"
8
+ height="20px"
9
+ width="20px"
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ >
12
+ <path d="M32 96v64h448V96H32zm0 128v64h448v-64H32zm0 128v64h448v-64H32z"></path>
13
+ </svg>
14
+ );
15
+ }
16
+ export function HomeIcon() {
17
+ return (
18
+ <svg
19
+ stroke="currentColor"
20
+ fill="currentColor"
21
+ strokeWidth="0"
22
+ viewBox="0 0 576 512"
23
+ height="20px"
24
+ width="20px"
25
+ xmlns="http://www.w3.org/2000/svg"
26
+ >
27
+ <path d="M280.37 148.26L96 300.11V464a16 16 0 0 0 16 16l112.06-.29a16 16 0 0 0 15.92-16V368a16 16 0 0 1 16-16h64a16 16 0 0 1 16 16v95.64a16 16 0 0 0 16 16.05L464 480a16 16 0 0 0 16-16V300L295.67 148.26a12.19 12.19 0 0 0-15.3 0zM571.6 251.47L488 182.56V44.05a12 12 0 0 0-12-12h-56a12 12 0 0 0-12 12v72.61L318.47 43a48 48 0 0 0-61 0L4.34 251.47a12 12 0 0 0-1.6 16.9l25.5 31A12 12 0 0 0 45.15 301l235.22-193.74a12.19 12.19 0 0 1 15.3 0L530.9 301a12 12 0 0 0 16.9-1.6l25.5-31a12 12 0 0 0-1.7-16.93z"></path>
28
+ </svg>
29
+ );
30
+ }
31
+ export function ArrowUpIcon() {
32
+ return (
33
+ <svg
34
+ stroke="currentColor"
35
+ fill="currentColor"
36
+ strokeWidth="0"
37
+ version="1.2"
38
+ baseProfile="tiny"
39
+ viewBox="0 0 24 24"
40
+ height="20px"
41
+ width="20px"
42
+ xmlns="http://www.w3.org/2000/svg"
43
+ >
44
+ <path d="M18.2 13.3l-6.2-6.3-6.2 6.3c-.2.2-.3.5-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7z"></path>
45
+ </svg>
46
+ );
47
+ }
48
+
49
+ export function ArrowDownIcon() {
50
+ return (
51
+ <svg
52
+ stroke="currentColor"
53
+ fill="currentColor"
54
+ strokeWidth="0"
55
+ version="1.2"
56
+ baseProfile="tiny"
57
+ viewBox="0 0 24 24"
58
+ height="20px"
59
+ width="20px"
60
+ xmlns="http://www.w3.org/2000/svg"
61
+ >
62
+ <path d="M5.8 9.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.4-.3.7s.1.5.3.7z"></path>
63
+ </svg>
64
+ );
65
+ }
@@ -0,0 +1,175 @@
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+ import { motion } from "framer-motion";
5
+ import MenuIcon, { ArrowDownIcon, ArrowUpIcon, HomeIcon } from "./icons";
6
+ import Link from "next/link";
7
+ type LocationItem = {
8
+ location: string;
9
+ name: React.ReactNode;
10
+ icon?: React.ReactNode;
11
+ };
12
+ export default function Container({
13
+ children,
14
+ appName,
15
+ menuList,
16
+ navItems,
17
+ leftPanel,
18
+ footPanel,
19
+ expandedFooter = false,
20
+ expandedMenu = false,
21
+ }: {
22
+ appName?: React.ReactNode;
23
+ children: React.ReactNode;
24
+ menuList?: Array<LocationItem>;
25
+ navItems?: Array<LocationItem>;
26
+ leftPanel?: React.ReactNode;
27
+ footPanel?: React.ReactNode;
28
+ expandedMenu?: boolean;
29
+ expandedFooter?: boolean;
30
+ }) {
31
+ const [isSidebarOpen, setIsSidebarOpen] = useState(expandedMenu);
32
+ const [isFooterOpen, setIsFooterOpen] = useState(expandedFooter);
33
+
34
+ return (
35
+ <div className="flex flex-col h-screen">
36
+ {/* Header */}
37
+ <header className="z-40">
38
+ <div className="bg-blue-600 text-white p-4 flex justify-between items-center shadow-md">
39
+ <button
40
+ onClick={() => setIsSidebarOpen(!isSidebarOpen)}
41
+ className="bg-blue-600 text-white px-2 py-1 rounded-r"
42
+ >
43
+ <MenuIcon />
44
+ </button>
45
+ <h1 className="text-xl font-bold">{appName}</h1>
46
+ </div>
47
+ <div
48
+ className={` gap-2 bg-gray-800 text-white ${
49
+ isSidebarOpen ? "px-[270px]" : "px-[60px]"
50
+ } hidden sm:flex`}
51
+ >
52
+ {[{ location: "/", name: "Home", icon: <HomeIcon /> }, navItems]
53
+ .flat()
54
+ .map((li, k) => {
55
+ return (
56
+ li && (
57
+ <>
58
+ {"/ "}
59
+ <Link href={li.location} key={k} className="flex gap-1 p-1">
60
+ {li.icon}
61
+ {li.name}
62
+ </Link>
63
+ </>
64
+ )
65
+ );
66
+ })}
67
+ </div>
68
+ </header>
69
+
70
+ <div className="flex flex-1 overflow-hidden relative">
71
+ {/* Sidebar como drawer en mobile */}
72
+ <motion.aside
73
+ animate={{
74
+ width:
75
+ typeof window !== "undefined" && window.innerWidth < 768
76
+ ? isSidebarOpen
77
+ ? "100%"
78
+ : 0
79
+ : isSidebarOpen
80
+ ? 250
81
+ : 60,
82
+ }}
83
+ className="bg-gray-800 text-white overflow-y-auto fixed md:static top-0 left-0 h-full z-50 md:z-auto transition-all duration-300 ease-in-out"
84
+ >
85
+ <div className="p-4 ">
86
+ {menuList && (
87
+ <ul className="space-y-3">
88
+ <li
89
+ key={"menu"}
90
+ className={"p-2 md:hidden "}
91
+ onClick={(e) => {
92
+ setIsSidebarOpen(false);
93
+ }}
94
+ >
95
+ <MenuIcon />
96
+ </li>
97
+ {menuList.map((itemMenu, k) => {
98
+ const letra = `${itemMenu?.name}`.split("")[0];
99
+ return (
100
+ <li key={k}>
101
+ <Link
102
+ href={itemMenu.location}
103
+ onClick={() => setIsSidebarOpen(false)}
104
+ >
105
+ {isSidebarOpen && (
106
+ <div className="flex gap-2 items-center hover:bg-gray-700 p-2 rounded">
107
+ {itemMenu.icon}
108
+ <div>{itemMenu.name}</div>
109
+ </div>
110
+ )}
111
+ {!isSidebarOpen && (
112
+ <div className="hover:bg-gray-200 hover:text-black rounded p-1">
113
+ {itemMenu?.icon || (
114
+ <span className="px-1 border shadow rounded">
115
+ {letra}
116
+ </span>
117
+ )}
118
+ </div>
119
+ )}
120
+ </Link>
121
+ </li>
122
+ );
123
+ })}
124
+ </ul>
125
+ )}
126
+ </div>
127
+ </motion.aside>
128
+
129
+ {/* Overlay para cerrar en móviles */}
130
+ {isSidebarOpen &&
131
+ typeof window !== "undefined" &&
132
+ window.innerWidth < 768 && (
133
+ <div
134
+ className="fixed inset-0 bg-black bg-opacity-40 z-40"
135
+ onClick={() => setIsSidebarOpen(false)}
136
+ />
137
+ )}
138
+
139
+ {/* Main */}
140
+ <main className="flex-1 overflow-auto p-4 bg-gray-100 z-10 md:ml-0 ">
141
+ <div className="flex md:flex-row flex-col h-full gap-4 ">
142
+ {leftPanel && (
143
+ <aside className="w-full md:w-64 flex-shrink-0 text-xs text-gray-600 bg-gray-200 p-5 border rounded">
144
+ {leftPanel}
145
+ </aside>
146
+ )}
147
+ <section className="flex-1">{children}</section>
148
+ </div>
149
+ </main>
150
+ </div>
151
+
152
+ {/* Footer */}
153
+ {footPanel && (
154
+ <motion.footer
155
+ className="bg-blue-100 overflow-hidden"
156
+ initial={isFooterOpen}
157
+ animate={{ height: isFooterOpen ? 120 : 40 }}
158
+ transition={{ duration: 0.3 }}
159
+ >
160
+ <div className="flex justify-center items-center p-2">
161
+ <button
162
+ onClick={() => setIsFooterOpen(!isFooterOpen)}
163
+ className="text-blue-700"
164
+ >
165
+ {isFooterOpen ? <ArrowDownIcon /> : <ArrowUpIcon />}
166
+ </button>
167
+ </div>
168
+ {isFooterOpen && (
169
+ <div className="px-4 pb-4 text-sm text-gray-600">{footPanel}</div>
170
+ )}
171
+ </motion.footer>
172
+ )}
173
+ </div>
174
+ );
175
+ }
@@ -0,0 +1,95 @@
1
+ import React, {
2
+ Dispatch,
3
+ Reducer,
4
+ SetStateAction,
5
+ useEffect,
6
+ useMemo,
7
+ useReducer,
8
+ useRef,
9
+ useState,
10
+ } from "react";
11
+ interface OnSubmitProps extends React.FormEvent<HTMLFormElement> {
12
+ values: Record<string, any>;
13
+ }
14
+ interface Props {
15
+ onSubmit: (e: OnSubmitProps) => void;
16
+ children: React.ReactNode;
17
+ state?: [any, Dispatch<SetStateAction<any>>];
18
+ invalidMessage?: string;
19
+ }
20
+
21
+ export function useFormValues<T>(initial: Partial<T>) {
22
+ function reducer(st: Partial<T>, action: Partial<T>) {
23
+ const newSt = { ...st, ...action };
24
+ return newSt;
25
+ }
26
+ const state = useReducer<Reducer<Partial<T>, Partial<T>>>(
27
+ reducer,
28
+ initial as Partial<T>
29
+ );
30
+ return state;
31
+ }
32
+ export default function Form({
33
+ onSubmit,
34
+ state,
35
+ invalidMessage = "Existen valores inválidos en el formulario",
36
+ children,
37
+ }: Props) {
38
+ const ref = useRef<HTMLFormElement>(null);
39
+ const [hvalues, setHValues] = state || useState({});
40
+ useEffect(() => {
41
+ if (ref?.current) {
42
+ const newValues = { ...hvalues };
43
+ const formData = new FormData(ref.current);
44
+ for (let [k, v] of formData.entries()) {
45
+ if (!newValues[k]) {
46
+ newValues[k] = "";
47
+ }
48
+ }
49
+ if (Object.keys(hvalues).length != Object.keys(newValues).length) {
50
+ setHValues(newValues);
51
+ }
52
+ }
53
+ }, [state, ref]);
54
+
55
+ function hasErrors() {
56
+ const hasInvalids = ref?.current?.querySelectorAll(".invalid");
57
+ if (hasInvalids && hasInvalids?.length > 0) {
58
+ return true;
59
+ }
60
+ return false;
61
+ }
62
+
63
+ return (
64
+ <form
65
+ ref={ref}
66
+ onSubmit={(e) => {
67
+ e.preventDefault();
68
+ if (hasErrors()) {
69
+ alert(invalidMessage);
70
+ return;
71
+ }
72
+ onSubmit && onSubmit({ ...e, values: hvalues });
73
+ }}
74
+ >
75
+ {React.Children.map(children, (child: any) => {
76
+ if (React.isValidElement(child)) {
77
+ const props: any = child?.props ? { ...child.props } : {};
78
+ try {
79
+ return React.cloneElement(child as any, {
80
+ value: hvalues[props.name] || "",
81
+ onChange: (e: any) => {
82
+ setHValues({ ...hvalues, [props.name]: e.target.value });
83
+ props?.onChange && props.onChange(e);
84
+ },
85
+ });
86
+ } catch (error) {
87
+ return child;
88
+ }
89
+ }
90
+
91
+ return child; // no modificar si no es input válido
92
+ })}
93
+ </form>
94
+ );
95
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,10 @@
1
+ export { default as Alert } from "./alert";
2
+ export { default as Button } from "./button";
3
+ export { default as Container } from "./container";
4
+ export { default as Form } from "./form";
5
+ export { default as Input } from "./input";
6
+ export { default as regularExpresions } from "./regular-expresions";
7
+ export { default as Table } from "./table";
8
+ export { default as TextArea } from "./text-area";
9
+ export { default as useResources } from "./use-resources";
10
+ export { default as Select } from "./select";
@@ -0,0 +1,43 @@
1
+ import { DetailedHTMLProps, InputHTMLAttributes } from "react";
2
+
3
+ interface InputProps
4
+ extends DetailedHTMLProps<
5
+ InputHTMLAttributes<HTMLInputElement>,
6
+ HTMLInputElement
7
+ > {
8
+ label: React.ReactNode;
9
+ regex?: RegExp;
10
+ invalidMessage?: React.ReactNode;
11
+ }
12
+ export default function Input({
13
+ label,
14
+ className,
15
+ regex,
16
+ invalidMessage = "Valor no válido",
17
+ ...props
18
+ }: InputProps) {
19
+ const value = `${props?.value || ""}`;
20
+ const isValid = !regex ? true : regex.test(value);
21
+
22
+ return (
23
+ <div className="w-full">
24
+ <label className="flex flex-col gap-1">
25
+ <div className="font-bold ">{label}</div>
26
+ <div>
27
+ <input
28
+ {...props}
29
+ className={[
30
+ "p-2 w-full rounded border shadow",
31
+ value != "" && !isValid && "bg-red-200 text-black",
32
+ value != "" && isValid && "bg-green-200 text-black",
33
+ className,
34
+ ].join(" ")}
35
+ />
36
+ </div>
37
+ </label>
38
+ {!isValid && value != "" && (
39
+ <div className="text-red-800 invalid">{invalidMessage}</div>
40
+ )}
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,11 @@
1
+ const regularExpresions: {
2
+ email: RegExp;
3
+ phone: RegExp;
4
+ number: RegExp;
5
+ } = {
6
+ email: /^[\w.-]+@[a-zA-Z\d.-]+\.[a-zA-Z]{2,}$/,
7
+ phone: /^\d{10}$/,
8
+ number: /^\d+$/,
9
+ };
10
+
11
+ export default regularExpresions;
@@ -0,0 +1,15 @@
1
+ export default function CloseIcon() {
2
+ return (
3
+ <svg
4
+ stroke="currentColor"
5
+ fill="currentColor"
6
+ strokeWidth="0"
7
+ viewBox="0 0 512 512"
8
+ height="20px"
9
+ width="20px"
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ >
12
+ <path d="M256 48C141.31 48 48 141.31 48 256s93.31 208 208 208 208-93.31 208-208S370.69 48 256 48zm86.63 272L320 342.63l-64-64-64 64L169.37 320l64-64-64-64L192 169.37l64 64 64-64L342.63 192l-64 64z"></path>
13
+ </svg>
14
+ );
15
+ }
@@ -0,0 +1,15 @@
1
+ export default function SelectIcon() {
2
+ return (
3
+ <svg
4
+ stroke="currentColor"
5
+ fill="currentColor"
6
+ strokeWidth="0"
7
+ viewBox="0 0 320 512"
8
+ height="20px"
9
+ width="20px"
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ >
12
+ <path d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z"></path>
13
+ </svg>
14
+ );
15
+ }